- {{ $strings.LabelItem }} |
- {{ $strings.LabelUser }} |
- {{ $strings.LabelPlayMethod }} |
- {{ $strings.LabelDeviceInfo }} |
- {{ $strings.LabelTimeListened }} |
- {{ $strings.LabelLastTime }} |
- {{ $strings.LabelLastUpdate }} |
+
+
+ |
+
+
+ {{ $getString('MessageSelected', [numSelected]) }}
+
+ {{ $strings.ButtonRemove }}
+
+ |
+
+
+ {{ $strings.LabelItem }} {{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}
+
+ |
+ {{ $strings.LabelUser }} |
+
+
+ {{ $strings.LabelPlayMethod }} {{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}
+
+ |
+ {{ $strings.LabelDeviceInfo }} |
+
+
+ {{ $strings.LabelTimeListened }} {{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}
+
+ |
+
+
+ {{ $strings.LabelLastTime }} {{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}
+
+ |
+
+
+ {{ $strings.LabelLastUpdate }} {{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}
+
+ |
-
-
+ |
+
+
+
+
+ |
+
{{ session.displayTitle }}
{{ session.displayAuthor }}
|
-
+ |
{{ filteredUserUsername }}
{{ session.user ? session.user.username : 'N/A' }}
|
-
+ |
{{ getPlayMethodName(session.playMethod) }}
|
-
+ |
|
-
+ |
{{ $elapsedPretty(session.timeListening) }}
|
-
+ |
{{ $secondsToTimestamp(session.currentTime) }}
|
@@ -45,10 +80,22 @@
|
-
-
-
Page {{ currentPage + 1 }} of {{ numPages }}
-
+
+
+
+
+
{{ $strings.LabelRowsPerPage }}
+
+
+
+
Page {{ currentPage + 1 }} of {{ numPages }}
+
+
+
+
+
+
+
{{ $strings.MessageNoListeningSessions }}
@@ -128,6 +175,7 @@ export default {
},
data() {
return {
+ loading: false,
showSessionModal: false,
selectedSession: null,
listeningSessions: [],
@@ -138,7 +186,11 @@ export default {
itemsPerPage: 10,
userFilter: null,
selectedUser: '',
- processingGoToTimestamp: false
+ sortBy: 'updatedAt',
+ sortDesc: true,
+ processingGoToTimestamp: false,
+ deletingSessions: false,
+ itemsPerPageOptions: [10, 25, 50, 100]
}
},
computed: {
@@ -162,9 +214,85 @@ export default {
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
+ },
+ numSelected() {
+ return this.listeningSessions.filter((s) => s.selected).length
+ },
+ isAllSelected: {
+ get() {
+ return this.numSelected === this.listeningSessions.length
+ },
+ set(val) {
+ this.setSelectionForAll(val)
+ }
}
},
methods: {
+ isSortSelected(column) {
+ return this.sortBy === column
+ },
+ sortColumn(column) {
+ if (this.sortBy === column) {
+ this.sortDesc = !this.sortDesc
+ } else {
+ this.sortBy = column
+ }
+ this.loadSessions(this.currentPage)
+ },
+ removeSelectedSessions() {
+ if (!this.numSelected) return
+ this.deletingSessions = true
+
+ let isAllSessions = this.isAllSelected
+ const payload = {
+ sessions: this.listeningSessions.filter((s) => s.selected).map((s) => s.id)
+ }
+ this.$axios
+ .$post(`/api/sessions/batch/delete`, payload)
+ .then(() => {
+ this.$toast.success('Sessions removed')
+ if (isAllSessions) {
+ // If all sessions were removed from the current page then go to the previous page
+ if (this.currentPage > 0) {
+ this.currentPage--
+ }
+ this.loadSessions(this.currentPage)
+ } else {
+ // Filter out the deleted sessions
+ this.listeningSessions = this.listeningSessions.filter((ls) => !payload.sessions.includes(ls.id))
+ }
+ })
+ .catch((error) => {
+ const errorMsg = error.response?.data || 'Failed to remove sessions'
+ this.$toast.error(errorMsg)
+ })
+ .finally(() => {
+ this.deletingSessions = false
+ })
+ },
+ removeSessionsClick() {
+ if (!this.numSelected) return
+ const payload = {
+ message: this.$getString('MessageConfirmRemoveListeningSessions', [this.numSelected]),
+ callback: (confirmed) => {
+ if (confirmed) {
+ this.removeSelectedSessions()
+ }
+ },
+ type: 'yesNo'
+ }
+ this.$store.commit('globals/setConfirmPrompt', payload)
+ },
+ setSelectionForAll(val) {
+ this.listeningSessions = this.listeningSessions.map((s) => {
+ s.selected = val
+ return s
+ })
+ },
+ updatedItemsPerPage() {
+ this.currentPage = 0
+ this.loadSessions(this.currentPage)
+ },
closedSession() {
this.loadOpenSessions()
},
@@ -252,6 +380,13 @@ export default {
nextPage() {
this.loadSessions(this.currentPage + 1)
},
+ clickSessionRow(session) {
+ if (this.numSelected > 0) {
+ session.selected = !session.selected
+ } else {
+ this.showSession(session)
+ }
+ },
showSession(session) {
this.selectedSession = session
this.showSessionModal = true
@@ -274,11 +409,21 @@ export default {
return 'Unknown'
},
async loadSessions(page) {
- const userFilterQuery = this.selectedUser ? `&user=${this.selectedUser}` : ''
- const data = await this.$axios.$get(`/api/sessions?page=${page}&itemsPerPage=${this.itemsPerPage}${userFilterQuery}`).catch((err) => {
+ this.loading = true
+ const urlSearchParams = new URLSearchParams()
+ urlSearchParams.set('page', page)
+ urlSearchParams.set('itemsPerPage', this.itemsPerPage)
+ urlSearchParams.set('sort', this.sortBy)
+ urlSearchParams.set('desc', this.sortDesc ? '1' : '0')
+ if (this.selectedUser) {
+ urlSearchParams.set('user', this.selectedUser)
+ }
+
+ const data = await this.$axios.$get(`/api/sessions?${urlSearchParams.toString()}`).catch((err) => {
console.error('Failed to load listening sessions', err)
return null
})
+ this.loading = false
if (!data) {
this.$toast.error('Failed to load listening sessions')
return
@@ -287,8 +432,13 @@ export default {
this.numPages = data.numPages
this.total = data.total
this.currentPage = data.page
- this.listeningSessions = data.sessions
- this.userFilter = data.userFilter
+ this.listeningSessions = data.sessions.map((ls) => {
+ return {
+ ...ls,
+ selected: false
+ }
+ })
+ this.userFilter = data.userId
},
async loadOpenSessions() {
const data = await this.$axios.$get('/api/sessions/open').catch((err) => {
@@ -326,15 +476,18 @@ export default {
.userSessionsTable tr:first-child {
background-color: #272727;
}
-.userSessionsTable tr:not(:first-child) {
+.userSessionsTable tr:not(:first-child):not(.selected) {
background-color: #373838;
}
-.userSessionsTable tr:not(:first-child):nth-child(odd) {
+.userSessionsTable tr:not(:first-child):nth-child(odd):not(.selected):not(:hover) {
background-color: #2f2f2f;
}
.userSessionsTable tr:hover:not(:first-child) {
background-color: #474747;
}
+.userSessionsTable tr.selected {
+ background-color: #474747;
+}
.userSessionsTable td {
padding: 4px 8px;
}
diff --git a/client/strings/cs.json b/client/strings/cs.json
index 6d39569e..bac376d8 100644
--- a/client/strings/cs.json
+++ b/client/strings/cs.json
@@ -406,6 +406,7 @@
"LabelRegion": "Region",
"LabelReleaseDate": "Datum vydání",
"LabelRemoveCover": "Odstranit obálku",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
"LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka",
"LabelRSSFeedOpen": "Otevření RSS kanálu",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "Opravdu chcete odstranit kolekci \"{0}\"?",
"MessageConfirmRemoveEpisode": "Opravdu chcete odstranit epizodu \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Opravdu chcete odstranit {0} epizody?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Opravdu chcete odebrat předčítání \"{0}\"?",
"MessageConfirmRemovePlaylist": "Opravdu chcete odstranit svůj playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Opravdu chcete přejmenovat žánr \"{0}\" na \"{1}\" pro všechny položky?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Opravdu chcete obnovit zálohu vytvořenou dne?",
"MessageRestoreBackupWarning": "Obnovení zálohy přepíše celou databázi umístěnou v /config a obálku obrázků v /metadata/items & /metadata/authors.
Backups nezmění žádné soubory ve složkách knihovny. Pokud jste povolili nastavení serveru pro ukládání obrázků obalu a metadat do složek knihovny, nebudou zálohovány ani přepsány.
Všichni klienti používající váš server budou automaticky obnoveni.",
"MessageSearchResultsFor": "Výsledky hledání pro",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server je nedostupný",
"MessageSetChaptersFromTracksDescription": "Nastavit kapitoly jako kapitolu a název kapitoly jako název zvukového souboru",
"MessageStartPlaybackAtTime": "Spustit přehrávání pro \"{0}\" v {1}?",
diff --git a/client/strings/da.json b/client/strings/da.json
index fa28dd24..3dd611d9 100644
--- a/client/strings/da.json
+++ b/client/strings/da.json
@@ -406,6 +406,7 @@
"LabelRegion": "Region",
"LabelReleaseDate": "Udgivelsesdato",
"LabelRemoveCover": "Fjern omslag",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
"LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn",
"LabelRSSFeedOpen": "Åben RSS-feed",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "Er du sikker på, at du vil fjerne samlingen \"{0}\"?",
"MessageConfirmRemoveEpisode": "Er du sikker på, at du vil fjerne episoden \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Er du sikker på, at du vil fjerne {0} episoder?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Er du sikker på, at du vil fjerne fortælleren \"{0}\"?",
"MessageConfirmRemovePlaylist": "Er du sikker på, at du vil fjerne din spilleliste \"{0}\"?",
"MessageConfirmRenameGenre": "Er du sikker på, at du vil omdøbe genre \"{0}\" til \"{1}\" for alle elementer?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Er du sikker på, at du vil gendanne sikkerhedskopien oprettet den",
"MessageRestoreBackupWarning": "Gendannelse af en sikkerhedskopi vil overskrive hele databasen, som er placeret på /config, og omslagsbilleder i /metadata/items & /metadata/authors.
Sikkerhedskopier ændrer ikke nogen filer i dine biblioteksmapper. Hvis du har aktiveret serverindstillinger for at gemme omslagskunst og metadata i dine biblioteksmapper, sikkerhedskopieres eller overskrives disse ikke.
Alle klienter, der bruger din server, opdateres automatisk.",
"MessageSearchResultsFor": "Søgeresultater for",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Serveren kunne ikke nås",
"MessageSetChaptersFromTracksDescription": "Indstil kapitler ved at bruge hver lydfil som et kapitel og kapiteloverskrift som lydfilnavn",
"MessageStartPlaybackAtTime": "Start afspilning for \"{0}\" kl. {1}?",
diff --git a/client/strings/de.json b/client/strings/de.json
index 6975b794..8cf54cbe 100644
--- a/client/strings/de.json
+++ b/client/strings/de.json
@@ -406,6 +406,7 @@
"LabelRegion": "Region",
"LabelReleaseDate": "Veröffentlichungsdatum",
"LabelRemoveCover": "Lösche Titelbild",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
"LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers",
"LabelRSSFeedOpen": "RSS Feed Offen",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "Sammlung \"{0}\" wird gelöscht! Sind Sie sicher?",
"MessageConfirmRemoveEpisode": "Episode \"{0}\" wird geloscht! Sind Sie sicher?",
"MessageConfirmRemoveEpisodes": "{0} Episoden werden gelöscht! Sind Sie sicher?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird gelöscht! Sind Sie sicher?",
"MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Sind Sie sicher?",
"MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Sind Sie sicher?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Sind Sie sicher, dass Sie die Sicherung wiederherstellen wollen, welche am",
"MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.
Bei der Sicherung werden keine Dateien in Ihren Bibliotheksordnern verändert. Wenn Sie die Servereinstellungen aktiviert haben, um Cover und Metadaten in Ihren Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.
Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
"MessageSearchResultsFor": "Suchergebnisse für",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
"MessageSetChaptersFromTracksDescription": "Kaitelerstellung basiert auf den existierenden einzelnen Audiodateien. Pro existierende Audiodatei wird 1 Kapitel erstellt, wobei deren Kapitelname aus dem Audiodateinamen extrahiert wird",
"MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?",
@@ -747,4 +750,4 @@
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
"ToastUserDeleteSuccess": "Benutzer gelöscht"
-}
+}
\ No newline at end of file
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index 02f9df05..f69175fd 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -406,6 +406,7 @@
"LabelRegion": "Region",
"LabelReleaseDate": "Release Date",
"LabelRemoveCover": "Remove cover",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed Open",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.
Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.
All clients using your server will be automatically refreshed.",
"MessageSearchResultsFor": "Search results for",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server could not be reached",
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
"MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",
@@ -747,4 +750,4 @@
"ToastSocketFailedToConnect": "Socket failed to connect",
"ToastUserDeleteFailed": "Failed to delete user",
"ToastUserDeleteSuccess": "User deleted"
-}
+}
\ No newline at end of file
diff --git a/client/strings/es.json b/client/strings/es.json
index 47315301..cefeb8f8 100644
--- a/client/strings/es.json
+++ b/client/strings/es.json
@@ -406,6 +406,7 @@
"LabelRegion": "Región",
"LabelReleaseDate": "Fecha de Estreno",
"LabelRemoveCover": "Remover Portada",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado",
"LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado",
"LabelRSSFeedOpen": "Fuente RSS Abierta",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "¿Está seguro de que desea remover la colección \"{0}\"?",
"MessageConfirmRemoveEpisode": "¿Está seguro de que desea remover el episodio \"{0}\"?",
"MessageConfirmRemoveEpisodes": "¿Está seguro de que desea remover {0} episodios?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "¿Está seguro de que desea remover el narrador \"{0}\"?",
"MessageConfirmRemovePlaylist": "¿Está seguro de que desea remover la lista de reproducción \"{0}\"?",
"MessageConfirmRenameGenre": "¿Está seguro de que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "¿Está seguro de que desea para restaurar del respaldo creado en",
"MessageRestoreBackupWarning": "Restaurar sobrescribirá toda la base de datos localizada en /config y las imágenes de portadas en /metadata/items y /metadata/authors.
El respaldo no modifica ningún archivo en las carpetas de su biblioteca. Si ha habilitado la opción del servidor para almacenar portadas y metadata en las carpetas de su biblioteca, esos archivos no se respaldan o sobrescriben.
Todos los clientes que usen su servidor se actualizarán automáticamente.",
"MessageSearchResultsFor": "Resultados de la búsqueda de",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "No se pudo establecer la conexión con el servidor",
"MessageSetChaptersFromTracksDescription": "Establecer capítulos usando cada archivo de audio como un capítulo y el título del capítulo como el nombre del archivo de audio",
"MessageStartPlaybackAtTime": "Iniciar reproducción para \"{0}\" en {1}?",
diff --git a/client/strings/fr.json b/client/strings/fr.json
index f6efa428..86a64602 100644
--- a/client/strings/fr.json
+++ b/client/strings/fr.json
@@ -406,6 +406,7 @@
"LabelRegion": "Région",
"LabelReleaseDate": "Date de parution",
"LabelRemoveCover": "Supprimer la couverture",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé",
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
"LabelRSSFeedOpen": "Flux RSS ouvert",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?",
"MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer l’épisode « {0} » ?",
"MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Êtes-vous sûr de vouloir supprimer le narrateur « {0} » ?",
"MessageConfirmRemovePlaylist": "Êtes-vous sûr de vouloir supprimer la liste de lecture « {0} » ?",
"MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » en « {1} » pour tous les articles ?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Êtes-vous certain de vouloir restaurer la sauvegarde créée le",
"MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items et /metadata/authors.
Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.
Tous les clients utilisant votre serveur seront automatiquement mis à jour.",
"MessageSearchResultsFor": "Résultats de recherche pour",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Serveur inaccessible",
"MessageSetChaptersFromTracksDescription": "Positionne un chapitre par fichier audio, avec le titre du fichier comme titre de chapitre",
"MessageStartPlaybackAtTime": "Démarrer la lecture pour « {0} » à {1} ?",
diff --git a/client/strings/gu.json b/client/strings/gu.json
index 0317e2f9..24c874eb 100644
--- a/client/strings/gu.json
+++ b/client/strings/gu.json
@@ -406,6 +406,7 @@
"LabelRegion": "Region",
"LabelReleaseDate": "Release Date",
"LabelRemoveCover": "Remove cover",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed Open",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.
Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.
All clients using your server will be automatically refreshed.",
"MessageSearchResultsFor": "Search results for",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server could not be reached",
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
"MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",
diff --git a/client/strings/hi.json b/client/strings/hi.json
index eb4f074f..e7ec6155 100644
--- a/client/strings/hi.json
+++ b/client/strings/hi.json
@@ -406,6 +406,7 @@
"LabelRegion": "Region",
"LabelReleaseDate": "Release Date",
"LabelRemoveCover": "Remove cover",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed Open",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.
Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.
All clients using your server will be automatically refreshed.",
"MessageSearchResultsFor": "Search results for",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server could not be reached",
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
"MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",
diff --git a/client/strings/hr.json b/client/strings/hr.json
index eb7d27d8..edefcf53 100644
--- a/client/strings/hr.json
+++ b/client/strings/hr.json
@@ -406,6 +406,7 @@
"LabelRegion": "Regija",
"LabelReleaseDate": "Datum izlaska",
"LabelRemoveCover": "Remove cover",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed Open",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "AJeste li sigurni da želite obrisati kolekciju \"{0}\"?",
"MessageConfirmRemoveEpisode": "Jeste li sigurni da želite obrisati epizodu \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Jeste li sigurni da želite obrisati {0} epizoda/-u?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Jeste li sigurni da želite povratiti backup kreiran",
"MessageRestoreBackupWarning": "Povračanje backupa će zamijeniti postoječu bazu podataka u /config i slike covera u /metadata/items i /metadata/authors.
Backups ne modificiraju nikakve datoteke u folderu od biblioteke. Ako imate uključene server postavke da spremate cover i metapodtake u folderu od biblioteke, onda oni neće biti backupani ili overwritten.
Svi klijenti koji koriste tvoj server će biti automatski osvježeni.",
"MessageSearchResultsFor": "Traži rezultate za",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server ne može biti kontaktiran",
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
"MessageStartPlaybackAtTime": "Pokreni reprodukciju za \"{0}\" na {1}?",
diff --git a/client/strings/it.json b/client/strings/it.json
index 7e526721..0860e83f 100644
--- a/client/strings/it.json
+++ b/client/strings/it.json
@@ -406,6 +406,7 @@
"LabelRegion": "Regione",
"LabelReleaseDate": "Data Release",
"LabelRemoveCover": "Rimuovi cover",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Email del proprietario personalizzato",
"LabelRSSFeedCustomOwnerName": "Nome del proprietario personalizzato",
"LabelRSSFeedOpen": "RSS Feed Aperto",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Sei sicuro di voler rimuovere il narratore \"{0}\"?",
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su",
"MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.
I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.
Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.",
"MessageSearchResultsFor": "cerca risultati per",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Impossibile raggiungere il server",
"MessageSetChaptersFromTracksDescription": "Impostare i capitoli utilizzando ciascun file audio come capitolo e il titolo del capitolo come nome del file audio",
"MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?",
diff --git a/client/strings/lt.json b/client/strings/lt.json
index 9c4b9a63..94067198 100644
--- a/client/strings/lt.json
+++ b/client/strings/lt.json
@@ -406,6 +406,7 @@
"LabelRegion": "Regionas",
"LabelReleaseDate": "Išleidimo data",
"LabelRemoveCover": "Pašalinti viršelį",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Pasirinktinis savininko el. paštas",
"LabelRSSFeedCustomOwnerName": "Pasirinktinis savininko vardas",
"LabelRSSFeedOpen": "Atidarytas RSS srautas",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "Ar tikrai norite pašalinti kolekciją \"{0}\"?",
"MessageConfirmRemoveEpisode": "Ar tikrai norite pašalinti epizodą \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Ar tikrai norite pašalinti {0} epizodus?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Ar tikrai norite pašalinti skaitytoją \"{0}\"?",
"MessageConfirmRemovePlaylist": "Ar tikrai norite pašalinti savo grojaraštį \"{0}\"?",
"MessageConfirmRenameGenre": "Ar tikrai norite pervadinti žanrą \"{0}\" į \"{1}\" visiems elementams?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Ar tikrai norite atkurti atsarginę kopiją, sukurtą",
"MessageRestoreBackupWarning": "Atkurdami atsarginę kopiją perrašysite visą duomenų bazę, esančią /config ir viršelių vaizdus /metadata/items ir /metadata/authors.
Atsarginės kopijos nekeičia jokių failų jūsų bibliotekos aplankuose. Jei esate įgalinę serverio nustatymus, kad viršelio meną ir metaduomenis saugotumėte savo bibliotekos aplankuose, šie neperrašomi ar atkuriami.
Visi klientai, naudojantys jūsų serverį, bus automatiškai atnaujinti.",
"MessageSearchResultsFor": "Paieškos rezultatai „{0}“",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Nepavyko pasiekti serverio",
"MessageSetChaptersFromTracksDescription": "Nustatyti skyrius, naudojant kiekvieną garso failą kaip skyrių ir skyriaus pavadinimą kaip garso failo pavadinimą",
"MessageStartPlaybackAtTime": "Paleisti klausymą „{0}“ nuo {1}?",
diff --git a/client/strings/nl.json b/client/strings/nl.json
index d4779abd..19a5c35a 100644
--- a/client/strings/nl.json
+++ b/client/strings/nl.json
@@ -406,6 +406,7 @@
"LabelRegion": "Regio",
"LabelReleaseDate": "Verschijningsdatum",
"LabelRemoveCover": "Verwijder cover",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Aangepast e-mailadres eigenaar",
"LabelRSSFeedCustomOwnerName": "Aangepaste naam eigenaar",
"LabelRSSFeedOpen": "RSS-feed open",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "Weet je zeker dat je de collectie \"{0}\" wil verwijderen?",
"MessageConfirmRemoveEpisode": "Weet je zeker dat je de aflevering \"{0}\" wil verwijderen?",
"MessageConfirmRemoveEpisodes": "Weet je zeker dat je {0} afleveringen wil verwijderen?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Weet je zeker dat je verteller \"{0}\" wil verwijderen?",
"MessageConfirmRemovePlaylist": "Weet je zeker dat je je afspeellijst \"{0}\" wil verwijderen?",
"MessageConfirmRenameGenre": "Weet je zeker dat je genre \"{0}\" wil hernoemen naar \"{1}\" voor alle onderdelen?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Weet je zeker dat je wil herstellen met behulp van de back-up gemaakt op",
"MessageRestoreBackupWarning": "Herstellen met een back-up zal de volledige database in /config en de covers in /metadata/items & /metadata/authors overschrijven.
Back-ups wijzigen geen bestanden in je bibliotheekmappen. Als je de serverinstelling gebruikt om covers en metadata in je bibliotheekmappen te bewaren dan worden deze niet geback-upt of overschreven.
Alle clients die van je server gebruik maken zullen automatisch worden ververst.",
"MessageSearchResultsFor": "Zoekresultaten voor",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server niet bereikbaar",
"MessageSetChaptersFromTracksDescription": "Stel hoofdstukken in met ieder audiobestand als een hoofdstuk en de audiobestandsnaam als hoofdstuktitel",
"MessageStartPlaybackAtTime": "Afspelen van \"{0}\" beginnen op {1}?",
diff --git a/client/strings/no.json b/client/strings/no.json
index 511c8b86..37cbc7a8 100644
--- a/client/strings/no.json
+++ b/client/strings/no.json
@@ -406,6 +406,7 @@
"LabelRegion": "Region",
"LabelReleaseDate": "Utgivelsesdato",
"LabelRemoveCover": "Fjern omslag",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Tilpasset eier Epost",
"LabelRSSFeedCustomOwnerName": "Tilpasset eier Navn",
"LabelRSSFeedOpen": "RSS Feed åpne",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "Er du sikker på at du vil fjerne samling\"{0}\"?",
"MessageConfirmRemoveEpisode": "Er du sikker på at du vil fjerne episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Er du sikker på at du vil fjerne {0} episoder?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Er du sikker på at du vil fjerne forteller \"{0}\"?",
"MessageConfirmRemovePlaylist": "Er du sikker på at du vil fjerne spillelisten \"{0}\"?",
"MessageConfirmRenameGenre": "Er du sikker på at du vil endre sjanger \"{0}\" til \"{1}\" for alle gjenstandene?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Er du sikker på at du vil gjenopprette sikkerhetskopien som var laget",
"MessageRestoreBackupWarning": "gjenoppretting av sikkerhetskopi vil overskrive hele databasen under /config og omslagsbilde under /metadata/items og /metadata/authors.
Sikkerhetskopier endrer ikke noen filer under dine bibliotekmapper. Hvis du har aktivert tjenerinstillingen for å lagre omslagsbilder og metadata i bibliotekmapper så vil ikke de filene bli tatt sikkerhetskopi eller overskrevet.
Alle klientene som bruker din tjener vil bli fornyet automatisk.",
"MessageSearchResultsFor": "Søk resultat for",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Tjener kunne ikke bli nådd",
"MessageSetChaptersFromTracksDescription": "Sett kapitler ved å bruke hver lydfil som kapittel og kapitteltittel som lydfilnavnet",
"MessageStartPlaybackAtTime": "Start avspilling av \"{0}\" ved {1}?",
diff --git a/client/strings/pl.json b/client/strings/pl.json
index b51084e9..86e29274 100644
--- a/client/strings/pl.json
+++ b/client/strings/pl.json
@@ -406,6 +406,7 @@
"LabelRegion": "Region",
"LabelReleaseDate": "Data wydania",
"LabelRemoveCover": "Remove cover",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed otwarty",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "Czy na pewno chcesz usunąć kolekcję \"{0}\"?",
"MessageConfirmRemoveEpisode": "Czy na pewno chcesz usunąć odcinek \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Czy na pewno chcesz usunąć {0} odcinki?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Czy na pewno chcesz przywrócić kopię zapasową utworzoną w dniu",
"MessageRestoreBackupWarning": "Przywrócenie kopii zapasowej spowoduje nadpisane bazy danych w folderze /config oraz okładke 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",
"MessageSearchResultsFor": "Wyniki wyszukiwania dla",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Nie udało się uzyskać połączenia z serwerem",
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
"MessageStartPlaybackAtTime": "Rozpoczęcie odtwarzania \"{0}\" od {1}?",
diff --git a/client/strings/ru.json b/client/strings/ru.json
index 03e7385f..4d26c4aa 100644
--- a/client/strings/ru.json
+++ b/client/strings/ru.json
@@ -406,6 +406,7 @@
"LabelRegion": "Регион",
"LabelReleaseDate": "Дата выхода",
"LabelRemoveCover": "Удалить обложку",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Пользовательский Email владельца",
"LabelRSSFeedCustomOwnerName": "Пользовательское Имя владельца",
"LabelRSSFeedOpen": "Открыть RSS-канал",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "Вы уверены, что хотите удалить коллекцию \"{0}\"?",
"MessageConfirmRemoveEpisode": "Вы уверены, что хотите удалить эпизод \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Вы уверены, что хотите удалить {0} эпизодов?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Вы уверены, что хотите удалить чтеца \"{0}\"?",
"MessageConfirmRemovePlaylist": "Вы уверены, что хотите удалить плейлист \"{0}\"?",
"MessageConfirmRenameGenre": "Вы уверены, что хотите переименовать жанр \"{0}\" в \"{1}\" для всех элементов?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Вы уверены, что хотите восстановить резервную копию, созданную",
"MessageRestoreBackupWarning": "Восстановление резервной копии перезапишет всю базу данных, расположенную в /config, и обложки изображений в /metadata/items и /metadata/authors.
Бэкапы не изменяют файлы в папках библиотеки. Если вы включили параметры сервера для хранения обложек и метаданных в папках библиотеки, то они не резервируются и не перезаписываются.
Все клиенты, использующие ваш сервер, будут автоматически обновлены.",
"MessageSearchResultsFor": "Результаты поиска для",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Не удалось связаться с сервером",
"MessageSetChaptersFromTracksDescription": "Установка глав с использованием каждого аудиофайла в качестве главы и заголовка главы в качестве имени аудиофайла",
"MessageStartPlaybackAtTime": "Начать воспроизведение для \"{0}\" с {1}?",
diff --git a/client/strings/sv.json b/client/strings/sv.json
index fde0cd87..1d71fce5 100644
--- a/client/strings/sv.json
+++ b/client/strings/sv.json
@@ -406,6 +406,7 @@
"LabelRegion": "Region",
"LabelReleaseDate": "Utgivningsdatum",
"LabelRemoveCover": "Ta bort omslag",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post",
"LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn",
"LabelRSSFeedOpen": "Öppna RSS-flöde",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "Är du säker på att du vill ta bort samlingen \"{0}\"?",
"MessageConfirmRemoveEpisode": "Är du säker på att du vill ta bort avsnittet \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Är du säker på att du vill ta bort {0} avsnitt?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Är du säker på att du vill ta bort berättaren \"{0}\"?",
"MessageConfirmRemovePlaylist": "Är du säker på att du vill ta bort din spellista \"{0}\"?",
"MessageConfirmRenameGenre": "Är du säker på att du vill byta namn på genren \"{0}\" till \"{1}\" för alla objekt?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "Är du säker på att du vill återställa säkerhetskopian som skapades den",
"MessageRestoreBackupWarning": "Att återställa en säkerhetskopia kommer att skriva över hela databasen som finns i /config och omslagsbilder i /metadata/items & /metadata/authors.
Säkerhetskopior ändrar inte några filer i dina biblioteksmappar. Om du har aktiverat serverinställningar för att lagra omslagskonst och metadata i dina biblioteksmappar säkerhetskopieras eller skrivs de inte över.
Alla klienter som använder din server kommer att uppdateras automatiskt.",
"MessageSearchResultsFor": "Sökresultat för",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Servern kunde inte nås",
"MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn",
"MessageStartPlaybackAtTime": "Starta uppspelning för \"{0}\" kl. {1}?",
diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json
index edf09040..05553c08 100644
--- a/client/strings/zh-cn.json
+++ b/client/strings/zh-cn.json
@@ -406,6 +406,7 @@
"LabelRegion": "区域",
"LabelReleaseDate": "发布日期",
"LabelRemoveCover": "移除封面",
+ "LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件",
"LabelRSSFeedCustomOwnerName": "自定义所有者名称",
"LabelRSSFeedOpen": "打开 RSS 源",
@@ -571,6 +572,7 @@
"MessageConfirmRemoveCollection": "你确定要移除收藏 \"{0}\"?",
"MessageConfirmRemoveEpisode": "你确定要移除剧集 \"{0}\"?",
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
+ "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "你确定要删除演播者 \"{0}\"?",
"MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?",
"MessageConfirmRenameGenre": "你确定要将所有项目流派 \"{0}\" 重命名到 \"{1}\"?",
@@ -650,6 +652,7 @@
"MessageRestoreBackupConfirm": "你确定要恢复创建的这个备份",
"MessageRestoreBackupWarning": "恢复备份将覆盖位于 /config 的整个数据库并覆盖 /metadata/items & /metadata/authors 中的图像.
备份不会修改媒体库文件夹中的任何文件. 如果您已启用服务器设置将封面和元数据存储在库文件夹中,则不会备份或覆盖这些内容.
将自动刷新使用服务器的所有客户端.",
"MessageSearchResultsFor": "搜索结果",
+ "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "无法访问服务器",
"MessageSetChaptersFromTracksDescription": "把每个音频文件设置为章节并将章节标题设置为音频文件名",
"MessageStartPlaybackAtTime": "开始播放 \"{0}\" 在 {1}?",
@@ -747,4 +750,4 @@
"ToastSocketFailedToConnect": "网络连接失败",
"ToastUserDeleteFailed": "删除用户失败",
"ToastUserDeleteSuccess": "用户已删除"
-}
+}
\ No newline at end of file
diff --git a/client/tailwind.config.js b/client/tailwind.config.js
index 1c39cb8f..9eb81923 100644
--- a/client/tailwind.config.js
+++ b/client/tailwind.config.js
@@ -56,6 +56,7 @@ module.exports = {
'16': '4rem',
'20': '5rem',
'24': '6rem',
+ '26': '6.5rem',
'32': '8rem',
'48': '12rem',
'64': '16rem',
diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js
index 884f0cd6..22fcaa1c 100644
--- a/server/controllers/SessionController.js
+++ b/server/controllers/SessionController.js
@@ -1,6 +1,6 @@
const Logger = require('../Logger')
const Database = require('../Database')
-const { toNumber } = require('../utils/index')
+const { toNumber, isUUID } = require('../utils/index')
class SessionController {
constructor() { }
@@ -9,35 +9,97 @@ class SessionController {
return res.json(req.playbackSession)
}
+ /**
+ * GET: /api/sessions
+ * @this import('../routers/ApiRouter')
+ *
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
async getAllWithUserData(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[SessionController] getAllWithUserData: Non-admin user requested all session data ${req.user.id}/"${req.user.username}"`)
return res.sendStatus(404)
}
-
- let listeningSessions = []
- if (req.query.user) {
- listeningSessions = await this.getUserListeningSessionsHelper(req.query.user)
- } else {
- listeningSessions = await this.getAllSessionsWithUserData()
+ // Validate "user" query
+ let userId = req.query.user
+ if (userId && !isUUID(userId)) {
+ Logger.warn(`[SessionController] Invalid "user" query string "${userId}"`)
+ userId = null
+ }
+ // Validate "sort" query
+ const validSortOrders = ['displayTitle', 'duration', 'playMethod', 'startTime', 'currentTime', 'timeListening', 'updatedAt', 'createdAt']
+ let orderKey = req.query.sort || 'updatedAt'
+ if (!validSortOrders.includes(orderKey)) {
+ Logger.warn(`[SessionController] Invalid "sort" query string "${orderKey}" (Must be one of "${validSortOrders.join('|')}")`)
+ orderKey = 'updatedAt'
+ }
+ let orderDesc = req.query.desc === '1' ? 'DESC' : 'ASC'
+ // Validate "itemsPerPage" and "page" query
+ let itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
+ if (itemsPerPage < 1) {
+ Logger.warn(`[SessionController] Invalid "itemsPerPage" query string "${itemsPerPage}"`)
+ itemsPerPage = 10
+ }
+ let page = toNumber(req.query.page, 0)
+ if (page < 0) {
+ Logger.warn(`[SessionController] Invalid "page" query string "${page}"`)
+ page = 0
}
- const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
- const page = toNumber(req.query.page, 0)
+ let where = null
+ const include = [
+ {
+ model: Database.models.device
+ }
+ ]
- const start = page * itemsPerPage
- const sessions = listeningSessions.slice(start, start + itemsPerPage)
+ if (userId) {
+ where = {
+ userId
+ }
+ } else {
+ include.push({
+ model: Database.userModel,
+ attributes: ['id', 'username']
+ })
+ }
+
+ const { rows, count } = await Database.playbackSessionModel.findAndCountAll({
+ where,
+ include,
+ order: [
+ [orderKey, orderDesc]
+ ],
+ limit: itemsPerPage,
+ offset: itemsPerPage * page
+ })
+
+ // Map playback sessions to old playback sessions
+ const sessions = rows.map(session => {
+ const oldPlaybackSession = Database.playbackSessionModel.getOldPlaybackSession(session)
+ if (session.user) {
+ return {
+ ...oldPlaybackSession,
+ user: {
+ id: session.user.id,
+ username: session.user.username
+ }
+ }
+ } else {
+ return oldPlaybackSession.toJSON()
+ }
+ })
const payload = {
- total: listeningSessions.length,
- numPages: Math.ceil(listeningSessions.length / itemsPerPage),
+ total: count,
+ numPages: Math.ceil(count / itemsPerPage),
page,
itemsPerPage,
sessions
}
-
- if (req.query.user) {
- payload.userFilter = req.query.user
+ if (userId) {
+ payload.userId = userId
}
res.json(payload)
@@ -92,6 +154,49 @@ class SessionController {
res.sendStatus(200)
}
+ /**
+ * POST: /api/sessions/batch/delete
+ * @this import('../routers/ApiRouter')
+ *
+ * @typedef batchDeleteReqBody
+ * @property {string[]} sessions
+ *
+ * @param {import('express').Request<{}, {}, batchDeleteReqBody, {}} req
+ * @param {import('express').Response} res
+ */
+ async batchDelete(req, res) {
+ if (!req.user.isAdminOrUp) {
+ Logger.error(`[SessionController] Non-admin user attempted to batch delete sessions "${req.user.username}"`)
+ return res.sendStatus(403)
+ }
+ // Validate session ids
+ if (!req.body.sessions?.length || !Array.isArray(req.body.sessions) || req.body.sessions.some(s => !isUUID(s))) {
+ Logger.error(`[SessionController] Invalid request body. "sessions" array is required`, req.body)
+ return res.status(400).send('Invalid request body. "sessions" array of session id strings is required.')
+ }
+
+ // Check if any of these sessions are open and close it
+ for (const sessionId of req.body.sessions) {
+ const openSession = this.playbackSessionManager.getSession(sessionId)
+ if (openSession) {
+ await this.playbackSessionManager.removeSession(sessionId)
+ }
+ }
+
+ try {
+ const sessionsRemoved = await Database.playbackSessionModel.destroy({
+ where: {
+ id: req.body.sessions
+ }
+ })
+ Logger.info(`[SessionController] ${sessionsRemoved} playback sessions removed by "${req.user.username}"`)
+ res.sendStatus(200)
+ } catch (error) {
+ Logger.error(`[SessionController] Failed to remove playback sessions`, error)
+ res.status(500).send('Failed to remove sessions')
+ }
+ }
+
// POST: api/session/local
syncLocal(req, res) {
this.playbackSessionManager.syncLocalSessionRequest(req, res)
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index f2418180..42e1d040 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -221,6 +221,7 @@ class ApiRouter {
this.router.get('/sessions', SessionController.getAllWithUserData.bind(this))
this.router.delete('/sessions/:id', SessionController.middleware.bind(this), SessionController.delete.bind(this))
this.router.get('/sessions/open', SessionController.getOpenSessions.bind(this))
+ this.router.post('/sessions/batch/delete', SessionController.batchDelete.bind(this))
this.router.post('/session/local', SessionController.syncLocal.bind(this))
this.router.post('/session/local-all', SessionController.syncLocalSessions.bind(this))
// TODO: Update these endpoints because they are only for open playback sessions
@@ -491,18 +492,6 @@ class ApiRouter {
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
}
- async getAllSessionsWithUserData() {
- const sessions = await Database.getPlaybackSessions()
- sessions.sort((a, b) => b.updatedAt - a.updatedAt)
- const minifiedUserObjects = await Database.userModel.getMinifiedUserObjects()
- return sessions.map(se => {
- return {
- ...se,
- user: minifiedUserObjects.find(u => u.id === se.userId) || null
- }
- })
- }
-
async getUserListeningStatsHelpers(userId) {
const today = date.format(new Date(), 'YYYY-MM-DD')
diff --git a/server/utils/index.js b/server/utils/index.js
index 29a65885..f75572ba 100644
--- a/server/utils/index.js
+++ b/server/utils/index.js
@@ -1,4 +1,5 @@
const Path = require('path')
+const uuid = require('uuid')
const Logger = require('../Logger')
const { parseString } = require("xml2js")
const areEquivalent = require('./areEquivalent')
@@ -220,4 +221,15 @@ module.exports.validateUrl = (rawUrl) => {
Logger.error(`Invalid URL "${rawUrl}"`, error)
return null
}
+}
+
+/**
+ * Check if a string is a valid UUID
+ *
+ * @param {string} str
+ * @returns {boolean}
+ */
+module.exports.isUUID = (str) => {
+ if (!str || typeof str !== 'string') return false
+ return uuid.validate(str)
}
\ No newline at end of file