Merge remote-tracking branch 'origin/develop' into feature/oidc-v2
# Conflicts: # docker-compose.yml # go.sum # internal/config/flags.go # internal/config/options.go
This commit is contained in:
commit
ca0abb1a95
250 changed files with 6550 additions and 4286 deletions
|
@ -1,4 +1,8 @@
|
|||
FROM photoprism/development:20210922
|
||||
FROM photoprism/development:20210929
|
||||
|
||||
# Copy latest entrypoint script
|
||||
COPY --chown=root:root /docker/development/entrypoint.sh /entrypoint.sh
|
||||
COPY --chown=root:root /docker/scripts/Makefile /root/Makefile
|
||||
|
||||
# Set up project directory
|
||||
WORKDIR "/go/src/github.com/photoprism/photoprism"
|
||||
|
|
23
README.md
23
README.md
|
@ -40,16 +40,17 @@ To get a first impression, you're welcome to play with our public demo at [demo.
|
|||
|
||||
Step-by-step installation instructions for our self-hosted [community edition](https://photoprism.app/get) can be found
|
||||
on [docs.photoprism.org](https://docs.photoprism.org/getting-started/) -
|
||||
all you need is a Web browser and Docker to run the server. It is available for Mac, Linux, and Windows.
|
||||
all you need is a Web browser and [Docker](https://docs.docker.com/get-docker/) to run the server.
|
||||
It is available for Mac, Linux, and Windows.
|
||||
|
||||
We recommend hosting PhotoPrism on a server with **at least 2 cores** and **4 GB of memory**.
|
||||
Also make sure it has at least 4 GB of swap configured, so that indexing doesn't cause
|
||||
restarts when there are memory usage spikes.
|
||||
Beyond these minimum requirements, the amount of RAM should match the number of cores.
|
||||
## New Release 🌈 ##
|
||||
|
||||
Indexing large photo and video collections significantly benefits from fast, local SSD storage,
|
||||
and lots of memory for caching. Especially the conversion of RAW images and the transcoding of
|
||||
videos are very demanding.
|
||||
The [latest release](https://docs.photoprism.org/release-notes/) not only includes
|
||||
**facial recognition**, it also comes as a
|
||||
**single [multi-arch image](https://hub.docker.com/r/photoprism/photoprism) for AMD64, ARM64, and ARMv7**.
|
||||
That means you don't need to pull from different Docker repositories anymore.
|
||||
We recommend updating your existing `docker-compose.yml` config based on
|
||||
[our examples](https://dl.photoprism.org/docker/).
|
||||
|
||||
## Roadmap ##
|
||||
|
||||
|
@ -79,13 +80,13 @@ In addition, you can find us on [Patreon](https://www.patreon.com/photoprism) an
|
|||
|
||||
Your continuous support helps...
|
||||
|
||||
* pay for operating expenses and external services like satellite maps
|
||||
* developing new features and keeping them free for everyone 🌈
|
||||
* pay for operating expenses and external services like satellite maps 🛰 🌏
|
||||
* **developing new features and keeping them free for everyone**
|
||||
|
||||
Also, please [leave a star](https://github.com/photoprism/photoprism/stargazers) on GitHub if you like this project.
|
||||
It provides additional motivation to keep going.
|
||||
|
||||
Thank you very much! <3
|
||||
Thank you very much! ❤️
|
||||
|
||||
<sup>1</sup> Ideas backed by one or more [eligible sponsors](SPONSORS.md) are marked with a [golden label](https://github.com/photoprism/photoprism/issues?q=is%3Aissue+is%3Aopen+label%3Asponsor).
|
||||
Let us know if we mistakenly [label an idea as unfunded](https://github.com/photoprism/photoprism/issues?q=is%3Aissue+is%3Aopen+label%3Aunfunded).
|
||||
|
|
Binary file not shown.
|
@ -7,280 +7,323 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-01-03 19:34+0000\n"
|
||||
"PO-Revision-Date: 2021-01-07 15:13+0100\n"
|
||||
"POT-Creation-Date: 2021-10-01 12:14+0000\n"
|
||||
"PO-Revision-Date: 2021-10-03 09:05+0200\n"
|
||||
"Last-Translator: Michael Mayer <michael@photoprism.org>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.4.2\n"
|
||||
"X-Generator: Poedit 3.0\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: messages.go:73
|
||||
#: messages.go:82
|
||||
msgid "Unexpected error, please try again"
|
||||
msgstr "Erreur imprévue, veuillez recommencer"
|
||||
|
||||
#: messages.go:74
|
||||
#: messages.go:83
|
||||
msgid "Invalid request"
|
||||
msgstr "Requête incorrecte"
|
||||
|
||||
#: messages.go:75
|
||||
#: messages.go:84
|
||||
msgid "Changes could not be saved"
|
||||
msgstr "Les modifications n'ont pas pu être sauvegardées"
|
||||
|
||||
#: messages.go:76
|
||||
#: messages.go:85
|
||||
msgid "Could not be deleted"
|
||||
msgstr "N'a pu être supprimé"
|
||||
|
||||
#: messages.go:77
|
||||
#: messages.go:86
|
||||
#, c-format
|
||||
msgid "%s already exists"
|
||||
msgstr "%s existe déjà"
|
||||
|
||||
#: messages.go:78 messages.go:81
|
||||
msgid "Not found on server, deleted?"
|
||||
msgstr "Non trouvé sur le serveur. Supprimé?"
|
||||
#: messages.go:87
|
||||
msgid "Not found"
|
||||
msgstr "Non trouvé"
|
||||
|
||||
#: messages.go:79
|
||||
#: messages.go:88
|
||||
msgid "File not found"
|
||||
msgstr "Fichier non trouvé"
|
||||
|
||||
#: messages.go:80
|
||||
#: messages.go:89
|
||||
msgid "Selection not found"
|
||||
msgstr "Sélection non trouvée"
|
||||
|
||||
#: messages.go:82
|
||||
#: messages.go:90
|
||||
msgid "Entity not found"
|
||||
msgstr "Entité non trouvée"
|
||||
|
||||
#: messages.go:91
|
||||
msgid "Account not found"
|
||||
msgstr "Compte non trouvé"
|
||||
|
||||
#: messages.go:83
|
||||
#: messages.go:92
|
||||
msgid "User not found"
|
||||
msgstr "Utilisateur non trouvé"
|
||||
|
||||
#: messages.go:84
|
||||
#: messages.go:93
|
||||
msgid "Label not found"
|
||||
msgstr "Étiquette non trouvée"
|
||||
|
||||
#: messages.go:85
|
||||
#: messages.go:94
|
||||
msgid "Album not found"
|
||||
msgstr "Album non trouvé"
|
||||
|
||||
#: messages.go:86
|
||||
#: messages.go:95
|
||||
msgid "Subject not found"
|
||||
msgstr "Sujet non trouvé"
|
||||
|
||||
#: messages.go:96
|
||||
msgid "Person not found"
|
||||
msgstr "Personne non trouvée"
|
||||
|
||||
#: messages.go:97
|
||||
msgid "Face not found"
|
||||
msgstr "Visage non trouvé"
|
||||
|
||||
#: messages.go:98
|
||||
msgid "Not available in public mode"
|
||||
msgstr "Non disponible en mode public"
|
||||
|
||||
#: messages.go:87
|
||||
#: messages.go:99
|
||||
msgid "not available in read-only mode"
|
||||
msgstr "non disponible en mode lecture seule"
|
||||
|
||||
#: messages.go:88
|
||||
#: messages.go:100
|
||||
msgid "Please log in and try again"
|
||||
msgstr "Veuillez vous identifier et recommencer"
|
||||
|
||||
#: messages.go:89
|
||||
#: messages.go:101
|
||||
msgid "Upload might be offensive"
|
||||
msgstr "Le chargement peut être choquant"
|
||||
|
||||
#: messages.go:90
|
||||
#: messages.go:102
|
||||
msgid "No items selected"
|
||||
msgstr "Aucun élément sélectionné"
|
||||
|
||||
#: messages.go:91
|
||||
#: messages.go:103
|
||||
msgid "Failed creating file, please check permissions"
|
||||
msgstr "Échec lors de la création du fichier, veuillez vérifier les autorisations"
|
||||
|
||||
#: messages.go:92
|
||||
#: messages.go:104
|
||||
msgid "Failed creating folder, please check permissions"
|
||||
msgstr "Échec lors de la création du dossier, veuillez vérifier les autorisations"
|
||||
|
||||
#: messages.go:93
|
||||
#: messages.go:105
|
||||
msgid "Could not connect, please try again"
|
||||
msgstr "Échec lors de la connexion, veuillez réessayer"
|
||||
|
||||
#: messages.go:94
|
||||
#: messages.go:106
|
||||
msgid "Invalid password, please try again"
|
||||
msgstr "Mot de passe incorrect, veuillez réessayer"
|
||||
|
||||
#: messages.go:95
|
||||
#: messages.go:107
|
||||
msgid "Feature disabled"
|
||||
msgstr "Fonctionnalité désactivée"
|
||||
|
||||
#: messages.go:96
|
||||
#: messages.go:108
|
||||
msgid "No labels selected"
|
||||
msgstr "Aucune étiquette sélectionnée"
|
||||
|
||||
#: messages.go:97
|
||||
#: messages.go:109
|
||||
msgid "No albums selected"
|
||||
msgstr "Aucun album sélectionné"
|
||||
|
||||
#: messages.go:98
|
||||
#: messages.go:110
|
||||
msgid "No files available for download"
|
||||
msgstr "Aucun fichier disponible au téléchargement"
|
||||
|
||||
#: messages.go:99
|
||||
#: messages.go:111
|
||||
msgid "Failed to create zip file"
|
||||
msgstr "Échec de la création de l'archive zip"
|
||||
|
||||
#: messages.go:100
|
||||
#: messages.go:112
|
||||
msgid "Invalid credentials"
|
||||
msgstr "Les informations d'identification sont invalides"
|
||||
|
||||
#: messages.go:101
|
||||
#: messages.go:113
|
||||
msgid "Invalid link"
|
||||
msgstr "Lien invalide"
|
||||
|
||||
#: messages.go:104
|
||||
#: messages.go:114
|
||||
msgid "Invalid name"
|
||||
msgstr "Nom incorrect"
|
||||
|
||||
#: messages.go:117
|
||||
msgid "Changes successfully saved"
|
||||
msgstr "Les modifications ont bien été enregistrées"
|
||||
|
||||
#: messages.go:105
|
||||
#: messages.go:118
|
||||
msgid "Album created"
|
||||
msgstr "Album créé"
|
||||
|
||||
#: messages.go:106
|
||||
#: messages.go:119
|
||||
msgid "Album saved"
|
||||
msgstr "Album sauvegardé"
|
||||
|
||||
#: messages.go:107
|
||||
#: messages.go:120
|
||||
#, c-format
|
||||
msgid "Album %s deleted"
|
||||
msgstr "Album %s supprimé"
|
||||
|
||||
#: messages.go:108
|
||||
#: messages.go:121
|
||||
msgid "Album contents cloned"
|
||||
msgstr "Le contenu de l'album a été cloné"
|
||||
|
||||
#: messages.go:109
|
||||
#: messages.go:122
|
||||
msgid "File removed from stack"
|
||||
msgstr "Fichier retiré du groupe"
|
||||
|
||||
#: messages.go:110
|
||||
#: messages.go:123
|
||||
msgid "File deleted"
|
||||
msgstr "Fichier supprimé"
|
||||
|
||||
#: messages.go:111
|
||||
#: messages.go:124
|
||||
#, c-format
|
||||
msgid "Selection added to %s"
|
||||
msgstr "Sélection ajoutée à %s"
|
||||
|
||||
#: messages.go:112
|
||||
#: messages.go:125
|
||||
#, c-format
|
||||
msgid "One entry added to %s"
|
||||
msgstr "Une entrée a été ajoutée à %s"
|
||||
|
||||
#: messages.go:113
|
||||
#: messages.go:126
|
||||
#, c-format
|
||||
msgid "%d entries added to %s"
|
||||
msgstr "%d entrées ont été ajoutées à %s"
|
||||
|
||||
#: messages.go:114
|
||||
#: messages.go:127
|
||||
#, c-format
|
||||
msgid "One entry removed from %s"
|
||||
msgstr "Une entrée a été supprimée de %s"
|
||||
|
||||
#: messages.go:115
|
||||
#: messages.go:128
|
||||
#, c-format
|
||||
msgid "%d entries removed from %s"
|
||||
msgstr "%d entrées ont été supprimées de %s"
|
||||
|
||||
#: messages.go:116
|
||||
#: messages.go:129
|
||||
msgid "Account created"
|
||||
msgstr "Compte créé"
|
||||
|
||||
#: messages.go:117
|
||||
#: messages.go:130
|
||||
msgid "Account saved"
|
||||
msgstr "Compte sauvegardé"
|
||||
|
||||
#: messages.go:118
|
||||
#: messages.go:131
|
||||
msgid "Account deleted"
|
||||
msgstr "Compte supprimé"
|
||||
|
||||
#: messages.go:119
|
||||
#: messages.go:132
|
||||
msgid "Settings saved"
|
||||
msgstr "Paramètres sauvegardés"
|
||||
|
||||
#: messages.go:120
|
||||
#: messages.go:133
|
||||
msgid "Password changed"
|
||||
msgstr "Mode de passe changé"
|
||||
|
||||
#: messages.go:121
|
||||
#: messages.go:134
|
||||
#, c-format
|
||||
msgid "Import completed in %d s"
|
||||
msgstr "Importation terminée en %d s"
|
||||
|
||||
#: messages.go:122
|
||||
#: messages.go:135
|
||||
msgid "Import canceled"
|
||||
msgstr "Importation annulée"
|
||||
|
||||
#: messages.go:123
|
||||
#: messages.go:136
|
||||
#, c-format
|
||||
msgid "Indexing completed in %d s"
|
||||
msgstr "Indexation terminée en %d s"
|
||||
|
||||
#: messages.go:124
|
||||
#: messages.go:137
|
||||
msgid "Indexing originals..."
|
||||
msgstr "Indexage des originaux…"
|
||||
|
||||
#: messages.go:125
|
||||
#: messages.go:138
|
||||
#, c-format
|
||||
msgid "Indexing files in %s"
|
||||
msgstr "Indexation des fichiers de %s"
|
||||
|
||||
#: messages.go:126
|
||||
#: messages.go:139
|
||||
msgid "Indexing canceled"
|
||||
msgstr "Indexation annulée"
|
||||
|
||||
#: messages.go:127
|
||||
#: messages.go:140
|
||||
#, c-format
|
||||
msgid "Removed %d files and %d photos"
|
||||
msgstr "Suppression de %d fichiers et %d photos"
|
||||
|
||||
#: messages.go:128
|
||||
#: messages.go:141
|
||||
#, c-format
|
||||
msgid "Moving files from %s"
|
||||
msgstr "Déplacement de fichiers depuis %s"
|
||||
|
||||
#: messages.go:129
|
||||
#: messages.go:142
|
||||
#, c-format
|
||||
msgid "Copying files from %s"
|
||||
msgstr "Copie de fichiers depuis %s"
|
||||
|
||||
#: messages.go:130
|
||||
#: messages.go:143
|
||||
msgid "Labels deleted"
|
||||
msgstr "Étiquettes supprimées"
|
||||
|
||||
#: messages.go:131
|
||||
#: messages.go:144
|
||||
msgid "Label saved"
|
||||
msgstr "Étiquettes sauvegardées"
|
||||
|
||||
#: messages.go:132
|
||||
#: messages.go:145
|
||||
msgid "Subject saved"
|
||||
msgstr "Sujet sauvegardé"
|
||||
|
||||
#: messages.go:146
|
||||
msgid "Subject deleted"
|
||||
msgstr "Sujet supprimé"
|
||||
|
||||
#: messages.go:147
|
||||
msgid "Person saved"
|
||||
msgstr "Personne sauvegardée"
|
||||
|
||||
#: messages.go:148
|
||||
msgid "Person deleted"
|
||||
msgstr "Personne supprimée"
|
||||
|
||||
#: messages.go:149
|
||||
#, c-format
|
||||
msgid "%d files uploaded in %d s"
|
||||
msgstr "%d fichiers chargés en %d s"
|
||||
|
||||
#: messages.go:133
|
||||
#: messages.go:150
|
||||
msgid "Selection approved"
|
||||
msgstr "Sélection approuvée"
|
||||
|
||||
#: messages.go:134
|
||||
#: messages.go:151
|
||||
msgid "Selection archived"
|
||||
msgstr "Sélection archivée"
|
||||
|
||||
#: messages.go:135
|
||||
#: messages.go:152
|
||||
msgid "Selection restored"
|
||||
msgstr "Sélection restaurée"
|
||||
|
||||
#: messages.go:136
|
||||
#: messages.go:153
|
||||
msgid "Selection marked as private"
|
||||
msgstr "Sélection marquée comme privée"
|
||||
|
||||
#: messages.go:137
|
||||
#: messages.go:154
|
||||
msgid "Albums deleted"
|
||||
msgstr "Albums supprimés"
|
||||
|
||||
#: messages.go:138
|
||||
#: messages.go:155
|
||||
#, c-format
|
||||
msgid "Zip created in %d s"
|
||||
msgstr "Archive zip créée en %d s"
|
||||
|
||||
#: messages.go:156
|
||||
msgid "Permanently deleted"
|
||||
msgstr "Supprimé définitivement"
|
||||
|
||||
#~ msgid "Not found on server, deleted?"
|
||||
#~ msgstr "Non trouvé sur le serveur. Supprimé?"
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-09-18 17:16+0000\n"
|
||||
"POT-Creation-Date: 2021-10-01 12:14+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -17,305 +17,309 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: messages.go:81
|
||||
#: messages.go:82
|
||||
msgid "Unexpected error, please try again"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:82
|
||||
#: messages.go:83
|
||||
msgid "Invalid request"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:83
|
||||
#: messages.go:84
|
||||
msgid "Changes could not be saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:84
|
||||
#: messages.go:85
|
||||
msgid "Could not be deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:85
|
||||
#: messages.go:86
|
||||
#, c-format
|
||||
msgid "%s already exists"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:86
|
||||
#: messages.go:87
|
||||
msgid "Not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:87
|
||||
#: messages.go:88
|
||||
msgid "File not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:88
|
||||
#: messages.go:89
|
||||
msgid "Selection not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:89
|
||||
#: messages.go:90
|
||||
msgid "Entity not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:90
|
||||
#: messages.go:91
|
||||
msgid "Account not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:91
|
||||
#: messages.go:92
|
||||
msgid "User not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:92
|
||||
#: messages.go:93
|
||||
msgid "Label not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:93
|
||||
#: messages.go:94
|
||||
msgid "Album not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:94
|
||||
#: messages.go:95
|
||||
msgid "Subject not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:95
|
||||
#: messages.go:96
|
||||
msgid "Person not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:96
|
||||
#: messages.go:97
|
||||
msgid "Face not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:97
|
||||
#: messages.go:98
|
||||
msgid "Not available in public mode"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:98
|
||||
#: messages.go:99
|
||||
msgid "not available in read-only mode"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:99
|
||||
#: messages.go:100
|
||||
msgid "Please log in and try again"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:100
|
||||
#: messages.go:101
|
||||
msgid "Upload might be offensive"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:101
|
||||
#: messages.go:102
|
||||
msgid "No items selected"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:102
|
||||
#: messages.go:103
|
||||
msgid "Failed creating file, please check permissions"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:103
|
||||
#: messages.go:104
|
||||
msgid "Failed creating folder, please check permissions"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:104
|
||||
#: messages.go:105
|
||||
msgid "Could not connect, please try again"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:105
|
||||
#: messages.go:106
|
||||
msgid "Invalid password, please try again"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:106
|
||||
#: messages.go:107
|
||||
msgid "Feature disabled"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:107
|
||||
#: messages.go:108
|
||||
msgid "No labels selected"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:108
|
||||
#: messages.go:109
|
||||
msgid "No albums selected"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:109
|
||||
#: messages.go:110
|
||||
msgid "No files available for download"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:110
|
||||
#: messages.go:111
|
||||
msgid "Failed to create zip file"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:111
|
||||
#: messages.go:112
|
||||
msgid "Invalid credentials"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:112
|
||||
#: messages.go:113
|
||||
msgid "Invalid link"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:115
|
||||
msgid "Changes successfully saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:116
|
||||
msgid "Album created"
|
||||
#: messages.go:114
|
||||
msgid "Invalid name"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:117
|
||||
msgid "Album saved"
|
||||
msgid "Changes successfully saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:118
|
||||
msgid "Album created"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:119
|
||||
msgid "Album saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:120
|
||||
#, c-format
|
||||
msgid "Album %s deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:119
|
||||
#: messages.go:121
|
||||
msgid "Album contents cloned"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:120
|
||||
#: messages.go:122
|
||||
msgid "File removed from stack"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:121
|
||||
msgid "File deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:122
|
||||
#, c-format
|
||||
msgid "Selection added to %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:123
|
||||
#, c-format
|
||||
msgid "One entry added to %s"
|
||||
msgid "File deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:124
|
||||
#, c-format
|
||||
msgid "%d entries added to %s"
|
||||
msgid "Selection added to %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:125
|
||||
#, c-format
|
||||
msgid "One entry removed from %s"
|
||||
msgid "One entry added to %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:126
|
||||
#, c-format
|
||||
msgid "%d entries removed from %s"
|
||||
msgid "%d entries added to %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:127
|
||||
msgid "Account created"
|
||||
#, c-format
|
||||
msgid "One entry removed from %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:128
|
||||
msgid "Account saved"
|
||||
#, c-format
|
||||
msgid "%d entries removed from %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:129
|
||||
msgid "Account deleted"
|
||||
msgid "Account created"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:130
|
||||
msgid "Settings saved"
|
||||
msgid "Account saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:131
|
||||
msgid "Password changed"
|
||||
msgid "Account deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:132
|
||||
#, c-format
|
||||
msgid "Import completed in %d s"
|
||||
msgid "Settings saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:133
|
||||
msgid "Import canceled"
|
||||
msgid "Password changed"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:134
|
||||
#, c-format
|
||||
msgid "Indexing completed in %d s"
|
||||
msgid "Import completed in %d s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:135
|
||||
msgid "Indexing originals..."
|
||||
msgid "Import canceled"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:136
|
||||
#, c-format
|
||||
msgid "Indexing files in %s"
|
||||
msgid "Indexing completed in %d s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:137
|
||||
msgid "Indexing canceled"
|
||||
msgid "Indexing originals..."
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:138
|
||||
#, c-format
|
||||
msgid "Removed %d files and %d photos"
|
||||
msgid "Indexing files in %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:139
|
||||
#, c-format
|
||||
msgid "Moving files from %s"
|
||||
msgid "Indexing canceled"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:140
|
||||
#, c-format
|
||||
msgid "Copying files from %s"
|
||||
msgid "Removed %d files and %d photos"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:141
|
||||
msgid "Labels deleted"
|
||||
#, c-format
|
||||
msgid "Moving files from %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:142
|
||||
msgid "Label saved"
|
||||
#, c-format
|
||||
msgid "Copying files from %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:143
|
||||
msgid "Subject saved"
|
||||
msgid "Labels deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:144
|
||||
msgid "Subject deleted"
|
||||
msgid "Label saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:145
|
||||
msgid "Person saved"
|
||||
msgid "Subject saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:146
|
||||
msgid "Person deleted"
|
||||
msgid "Subject deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:147
|
||||
msgid "Person saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:148
|
||||
msgid "Person deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:149
|
||||
#, c-format
|
||||
msgid "%d files uploaded in %d s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:148
|
||||
#: messages.go:150
|
||||
msgid "Selection approved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:149
|
||||
#: messages.go:151
|
||||
msgid "Selection archived"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:150
|
||||
#: messages.go:152
|
||||
msgid "Selection restored"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:151
|
||||
#: messages.go:153
|
||||
msgid "Selection marked as private"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:152
|
||||
#: messages.go:154
|
||||
msgid "Albums deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:153
|
||||
#: messages.go:155
|
||||
#, c-format
|
||||
msgid "Zip created in %d s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:154
|
||||
#: messages.go:156
|
||||
msgid "Permanently deleted"
|
||||
msgstr ""
|
||||
|
|
|
@ -55,25 +55,25 @@ func main() {
|
|||
app.Commands = []cli.Command{
|
||||
commands.StartCommand,
|
||||
commands.StopCommand,
|
||||
commands.StatusCommand,
|
||||
commands.IndexCommand,
|
||||
commands.ImportCommand,
|
||||
commands.CopyCommand,
|
||||
commands.FacesCommand,
|
||||
commands.MomentsCommand,
|
||||
commands.OptimizeCommand,
|
||||
commands.PurgeCommand,
|
||||
commands.CleanUpCommand,
|
||||
commands.CopyCommand,
|
||||
commands.OptimizeCommand,
|
||||
commands.MomentsCommand,
|
||||
commands.ConvertCommand,
|
||||
commands.ResampleCommand,
|
||||
commands.ThumbsCommand,
|
||||
commands.MigrateCommand,
|
||||
commands.BackupCommand,
|
||||
commands.RestoreCommand,
|
||||
commands.ResetCommand,
|
||||
commands.ConfigCommand,
|
||||
commands.UsersCommand,
|
||||
commands.PasswdCommand,
|
||||
commands.UsersCommand,
|
||||
commands.ConfigCommand,
|
||||
commands.VersionCommand,
|
||||
commands.StatusCommand,
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
|
|
|
@ -40,6 +40,7 @@ services:
|
|||
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
|
||||
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
|
||||
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
|
||||
PHOTOPRISM_DISABLE_CHOWN: "true" # Disables storage permission updates on startup
|
||||
PHOTOPRISM_DISABLE_BACKUPS: "false" # Don't backup photo and album metadata to YAML files
|
||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server
|
||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # Disables Settings in Web UI
|
||||
|
|
|
@ -10,7 +10,8 @@ services:
|
|||
ports:
|
||||
- "2344:2342" # [server]:[container]
|
||||
environment:
|
||||
UID: ${UID:-1000}
|
||||
PHOTOPRISM_UID: ${UID:-1000}
|
||||
PHOTOPRISM_GID: ${GID:-1000}
|
||||
PHOTOPRISM_SITE_URL: "http://localhost:2344/"
|
||||
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
|
||||
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
|
||||
|
@ -30,6 +31,7 @@ services:
|
|||
PHOTOPRISM_DATABASE_USER: "root"
|
||||
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
|
||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # The initial admin password (min 4 characters)
|
||||
PHOTOPRISM_DISABLE_CHOWN: "false" # Disables storage permission updates on startup
|
||||
PHOTOPRISM_DISABLE_BACKUPS: "false" # Don't backup photo and album metadata to YAML files
|
||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server
|
||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # Disables Settings in Web UI
|
||||
|
|
|
@ -14,13 +14,10 @@ services:
|
|||
- "2342:2342" # Web Server (PhotoPrism)
|
||||
- "2343:2343" # Acceptance Tests
|
||||
- "40000:40000" # Go Debugging
|
||||
working_dir: "/go/src/github.com/photoprism/photoprism"
|
||||
volumes:
|
||||
- ".:/go/src/github.com/photoprism/photoprism"
|
||||
- "go-mod:/go/pkg/mod"
|
||||
shm_size: "2gb"
|
||||
environment:
|
||||
UID: ${UID:-1000}
|
||||
PHOTOPRISM_UID: ${UID:-1000}
|
||||
PHOTOPRISM_GID: ${GID:-1000}
|
||||
PHOTOPRISM_SITE_URL: "http://localhost:2342/"
|
||||
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
|
||||
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
|
||||
|
@ -28,12 +25,12 @@ services:
|
|||
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"
|
||||
PHOTOPRISM_DEBUG: "true"
|
||||
PHOTOPRISM_READONLY: "false"
|
||||
PHOTOPRISM_PUBLIC: "false"
|
||||
PHOTOPRISM_PUBLIC: "true"
|
||||
PHOTOPRISM_EXPERIMENTAL: "true"
|
||||
PHOTOPRISM_SERVER_MODE: "debug"
|
||||
PHOTOPRISM_HTTP_HOST: "0.0.0.0"
|
||||
PHOTOPRISM_HTTP_PORT: 2342
|
||||
PHOTOPRISM_HTTP_COMPRESSION: "gzip" # Improves transfer speed and bandwidth utilization (none or gzip)
|
||||
PHOTOPRISM_HTTP_COMPRESSION: "gzip" # Improves transfer speed and bandwidth utilization (none or gzip)
|
||||
PHOTOPRISM_DATABASE_DRIVER: "mysql"
|
||||
PHOTOPRISM_DATABASE_SERVER: "mariadb:4001"
|
||||
PHOTOPRISM_DATABASE_NAME: "photoprism"
|
||||
|
@ -41,35 +38,52 @@ services:
|
|||
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
|
||||
PHOTOPRISM_TEST_DRIVER: "sqlite"
|
||||
PHOTOPRISM_TEST_DSN: ".test.db"
|
||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # The initial admin password (min 4 characters)
|
||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # The initial admin password (min 4 characters)
|
||||
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
|
||||
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
|
||||
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
|
||||
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
|
||||
PHOTOPRISM_DISABLE_BACKUPS: "false" # Don't backup photo and album metadata to YAML files
|
||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server
|
||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # Disables Settings in Web UI
|
||||
PHOTOPRISM_DISABLE_PLACES: "false" # Disables reverse geocoding and maps
|
||||
PHOTOPRISM_DISABLE_EXIFTOOL: "false" # Don't create ExifTool JSON files for improved metadata extraction
|
||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # Don't use TensorFlow for image classification
|
||||
PHOTOPRISM_DETECT_NSFW: "false" # Flag photos as private that MAY be offensive (requires TensorFlow)
|
||||
PHOTOPRISM_UPLOAD_NSFW: "false" # Allows uploads that may be offensive
|
||||
PHOTOPRISM_DARKTABLE_PRESETS: "false" # Enables Darktable presets and disables concurrent RAW conversion
|
||||
PHOTOPRISM_THUMB_FILTER: "lanczos" # Resample filter, best to worst: blackman, lanczos, cubic, linear
|
||||
PHOTOPRISM_THUMB_UNCACHED: "true" # Enables on-demand thumbnail rendering (high memory and cpu usage)
|
||||
PHOTOPRISM_THUMB_SIZE: 2048 # Pre-rendered thumbnail size limit (default 2048, min 720, max 7680)
|
||||
# PHOTOPRISM_THUMB_SIZE: 4096 # Retina 4K, DCI 4K (requires more storage); 7680 for 8K Ultra HD
|
||||
PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # On-demand rendering size limit (default 7680, min 720, max 7680)
|
||||
PHOTOPRISM_JPEG_SIZE: 7680 # Size limit for converted image files in pixels (720-30000)
|
||||
PHOTOPRISM_JPEG_QUALITY: 92 # Set to 95 for high-quality thumbnails (25-100)
|
||||
TF_CPP_MIN_LOG_LEVEL: 0 # Show TensorFlow log messages for development
|
||||
HOME: "/photoprism"
|
||||
PHOTOPRISM_DISABLE_CHOWN: "false" # Disables storage permission updates on startup
|
||||
PHOTOPRISM_DISABLE_BACKUPS: "false" # Don't backup photo and album metadata to YAML files
|
||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server
|
||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # Disables Settings in Web UI
|
||||
PHOTOPRISM_DISABLE_PLACES: "false" # Disables reverse geocoding and maps
|
||||
PHOTOPRISM_DISABLE_EXIFTOOL: "false" # Don't create ExifTool JSON files for improved metadata extraction
|
||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # Don't use TensorFlow for image classification
|
||||
PHOTOPRISM_DETECT_NSFW: "false" # Flag photos as private that MAY be offensive (requires TensorFlow)
|
||||
PHOTOPRISM_UPLOAD_NSFW: "false" # Allows uploads that may be offensive
|
||||
PHOTOPRISM_DARKTABLE_PRESETS: "false" # Enables Darktable presets and disables concurrent RAW conversion
|
||||
PHOTOPRISM_THUMB_FILTER: "lanczos" # Resample filter, best to worst: blackman, lanczos, cubic, linear
|
||||
PHOTOPRISM_THUMB_UNCACHED: "true" # Enables on-demand thumbnail rendering (high memory and cpu usage)
|
||||
PHOTOPRISM_THUMB_SIZE: 2048 # Pre-rendered thumbnail size limit (default 2048, min 720, max 7680)
|
||||
# PHOTOPRISM_THUMB_SIZE: 4096 # Retina 4K, DCI 4K (requires more storage); 7680 for 8K Ultra HD
|
||||
PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # On-demand rendering size limit (default 7680, min 720, max 7680)
|
||||
PHOTOPRISM_JPEG_SIZE: 7680 # Size limit for converted image files in pixels (720-30000)
|
||||
PHOTOPRISM_JPEG_QUALITY: 92 # Set to 95 for high-quality thumbnails (25-100)
|
||||
PHOTOPRISM_OIDC_ISSUER: "https://keycloak.timovolkmann.de/auth/realms/master"
|
||||
PHOTOPRISM_OIDC_CLIENT_ID: "photoprism-dev"
|
||||
PHOTOPRISM_OIDC_CLIENT_SECRET: "9d8351a0-ca01-4556-9c37-85eb634869b9"
|
||||
# PHOTOPRISM_OIDC_ISSUER: "https://accounts.google.com"
|
||||
# PHOTOPRISM_OIDC_CLIENT_ID: "86720117204-mb09c300nas5r9rid1ad0omv67nlvhck.apps.googleusercontent.com"
|
||||
# PHOTOPRISM_OIDC_CLIENT_SECRET: "WQ2-LdfhYhHd-BdpfZCYGE12"
|
||||
# PHOTOPRISM_OIDC_ISSUER: "https://accounts.google.com"
|
||||
# PHOTOPRISM_OIDC_CLIENT_ID: "86720117204-mb09c300nas5r9rid1ad0omv67nlvhck.apps.googleusercontent.com"
|
||||
# PHOTOPRISM_OIDC_CLIENT_SECRET: "WQ2-LdfhYhHd-BdpfZCYGE12"
|
||||
TF_CPP_MIN_LOG_LEVEL: 0 # Show TensorFlow log messages for development
|
||||
# Enable TensorFlow AVX2 support for modern Intel CPUs (requires starting the container as root):
|
||||
# PHOTOPRISM_INIT: "tensorflow-amd64-avx2"
|
||||
# Hardware video transcoding options:
|
||||
# PHOTOPRISM_FFMPEG_BUFFERS: "64" # FFmpeg capture buffers (default: 32)
|
||||
# PHOTOPRISM_FFMPEG_BITRATE: "32" # FFmpeg encoding bitrate limit in Mbit/s (default: 50)
|
||||
# PHOTOPRISM_FFMPEG_ENCODER: "h264_v4l2m2m" # Use Video4Linux for AVC transcoding (default: libx264)
|
||||
# PHOTOPRISM_FFMPEG_ENCODER: "h264_qsv" # Use Intel Quick Sync Video for AVC transcoding (default: libx264)
|
||||
# PHOTOPRISM_INIT: "intel-graphics tensorflow-amd64-avx2" # Enable TensorFlow AVX2 & Intel Graphics support
|
||||
# Optional hardware devices for video transcoding and machine learning:
|
||||
# devices:
|
||||
# - "/dev/video11:/dev/video11" # Video4Linux (h264_v4l2m2m)
|
||||
# - "/dev/dri/renderD128:/dev/dri/renderD128" # Intel GPU
|
||||
# - "/dev/dri/card0:/dev/dri/card0"
|
||||
working_dir: "/go/src/github.com/photoprism/photoprism"
|
||||
volumes:
|
||||
- ".:/go/src/github.com/photoprism/photoprism"
|
||||
- "go-mod:/go/pkg/mod"
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10.5
|
||||
|
@ -92,8 +106,8 @@ services:
|
|||
oidc-test-op:
|
||||
build: docker/oidc/.
|
||||
image: test-op:latest
|
||||
ports:
|
||||
- "9998:9998"
|
||||
# ports:
|
||||
# - "9998:9998"
|
||||
|
||||
volumes:
|
||||
go-mod:
|
||||
|
|
|
@ -21,7 +21,7 @@ RUN echo 'Acquire::Retries "10";' > /etc/apt/apt.conf.d/80retry && \
|
|||
COPY --chown=root:root --chmod=755 /docker/scripts/*.sh /root/.local/bin/
|
||||
|
||||
# Install dev / build dependencies
|
||||
RUN apt-get update && apt dist-upgrade 2>/dev/null && apt-get -qq install -y --no-install-recommends \
|
||||
RUN apt-get update && apt-get -qq dist-upgrade && apt-get -qq install --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
wget \
|
||||
|
@ -116,8 +116,11 @@ RUN rm -rf /tmp/* && mkdir -p /tmp/photoprism && \
|
|||
wget "https://dl.photoprism.org/tensorflow/facenet.zip?${BUILD_TAG}" -O /tmp/photoprism/facenet.zip && \
|
||||
wget "https://dl.photoprism.org/qa/testdata.zip?${BUILD_TAG}" -O /tmp/photoprism/testdata.zip
|
||||
|
||||
# Install additional tools
|
||||
COPY --chown=root:root --chmod=755 /docker/scripts/heif-convert.sh /usr/local/bin/heif-convert
|
||||
# Copy additional files to image
|
||||
COPY --chown=root:root /docker/scripts/heif-convert.sh /usr/local/bin/heif-convert
|
||||
COPY --chown=root:root /docker/scripts/Makefile /root/Makefile
|
||||
COPY --chown=root:root /docker/development/entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN env GO111MODULE=off /usr/local/go/bin/go get -u github.com/tianon/gosu \
|
||||
golang.org/x/tools/cmd/goimports github.com/kyoh86/richgo && \
|
||||
[ "$TARGETARCH" = "arm" ] || \
|
||||
|
@ -127,12 +130,13 @@ RUN env GO111MODULE=off /usr/local/go/bin/go get -u github.com/tianon/gosu \
|
|||
cp /go/bin/gosu /bin/gosu
|
||||
|
||||
# Create photoprism user and directory for deployment
|
||||
RUN useradd photoprism -m -d /photoprism && chmod a+rwx /photoprism && \
|
||||
RUN useradd -m -U -u 1000 -d /photoprism photoprism && chmod a+rwx /photoprism && \
|
||||
mkdir -m 777 -p /var/lib/photoprism /tmp/photoprism && \
|
||||
echo "alias go=richgo" > /photoprism/.bash_aliases && \
|
||||
echo "photoprism ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \
|
||||
chown -Rf photoprism:photoprism /photoprism /var/lib/photoprism /tmp/photoprism && \
|
||||
chmod -Rf a+rw /photoprism /var/lib/photoprism /tmp/photoprism /go && \
|
||||
chmod 755 /usr/local/bin/heif-convert /entrypoint.sh && \
|
||||
find /go -type d -print0 | xargs -0 chmod 777
|
||||
|
||||
# Copy mysql client config for development
|
||||
|
@ -148,7 +152,6 @@ EXPOSE 2342 2343 9515
|
|||
VOLUME /var/lib/photoprism
|
||||
|
||||
# Configure entrypoint
|
||||
COPY --chown=root:root --chmod=755 /docker/development/entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
# Run server
|
||||
|
|
|
@ -1,39 +1,89 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
if [[ ${UMASK} ]]; then
|
||||
echo "umask ${UMASK}"
|
||||
umask "${UMASK}"
|
||||
if [[ $(id -u) == "0" ]]; then
|
||||
echo "development base image started as root"
|
||||
|
||||
if [ -e /root/.init ]; then
|
||||
echo "initialized"
|
||||
elif [[ ${PHOTOPRISM_INIT} ]]; then
|
||||
for target in $PHOTOPRISM_INIT; do
|
||||
echo "init ${target}..."
|
||||
make -f /root/Makefile "${target}"
|
||||
done
|
||||
echo 1 >/root/.init
|
||||
fi
|
||||
else
|
||||
echo "development base image started as uid $(id -u)"
|
||||
fi
|
||||
|
||||
find /go -type d -print0 | xargs -0 chmod 777
|
||||
chmod -Rf a+rw /var/lib/photoprism /tmp/photoprism /go
|
||||
re='^[0-9]+$'
|
||||
|
||||
if [[ ${UID} ]] && [[ ${GID} ]] && [[ ${UID} != "0" ]] && [[ $(id -u) = "0" ]]; then
|
||||
groupadd -f -g "${GID}" "${GID}"
|
||||
usermod -o -u "${UID}" -g "${GID}" photoprism
|
||||
# Legacy umask env variable in use?
|
||||
if [[ -z ${PHOTOPRISM_UMASK} ]] && [[ ${UMASK} =~ $re ]]; then
|
||||
PHOTOPRISM_UMASK=${UMASK}
|
||||
echo "WARNING: UMASK without PHOTOPRISM_ prefix is deprecated, use PHOTOPRISM_UMASK: \"${PHOTOPRISM_UMASK}\" instead"
|
||||
fi
|
||||
|
||||
if [[ -z ${PHOTOPRISM_DISABLE_CHOWN} ]] ; then
|
||||
echo "updating storage permissions..."
|
||||
chown -Rf photoprism /photoprism /var/lib/photoprism /tmp/photoprism /go
|
||||
# Set file permission mask
|
||||
if [[ ${PHOTOPRISM_UMASK} =~ $re ]]; then
|
||||
echo "umask ${PHOTOPRISM_UMASK}"
|
||||
umask "${PHOTOPRISM_UMASK}"
|
||||
fi
|
||||
|
||||
# Script runs as root?
|
||||
if [[ $(id -u) == "0" ]]; then
|
||||
# Legacy user ID env variable in use?
|
||||
if [[ -z ${PHOTOPRISM_UID} ]] && [[ ${UID} =~ $re ]] && [[ ${UID} != "0" ]]; then
|
||||
PHOTOPRISM_UID=${UID}
|
||||
echo "WARNING: UID without PHOTOPRISM_ prefix is deprecated, use PHOTOPRISM_UID: \"${PHOTOPRISM_UID}\" instead"
|
||||
fi
|
||||
|
||||
echo "running as uid ${UID}:${GID}"
|
||||
echo "${@}"
|
||||
|
||||
gosu "${UID}:${GID}" "$@" &
|
||||
elif [[ ${UID} ]] && [[ ${UID} != "0" ]] && [[ $(id -u) = "0" ]]; then
|
||||
usermod -o -u "${UID}" photoprism
|
||||
|
||||
if [[ -z ${PHOTOPRISM_DISABLE_CHOWN} ]] ; then
|
||||
echo "updating storage permissions..."
|
||||
chown -Rf photoprism /photoprism /var/lib/photoprism /tmp/photoprism /go
|
||||
# Legacy group ID env variable in use?
|
||||
if [[ -z ${PHOTOPRISM_GID} ]] && [[ ${GID} =~ $re ]] && [[ ${GID} != "0" ]]; then
|
||||
PHOTOPRISM_GID=${GID}
|
||||
echo "WARNING: GID without PHOTOPRISM_ prefix is deprecated, use PHOTOPRISM_GID: \"${PHOTOPRISM_GID}\" instead"
|
||||
fi
|
||||
|
||||
echo "running as uid ${UID}"
|
||||
echo "${@}"
|
||||
# User and group ID set?
|
||||
if [[ ${PHOTOPRISM_UID} =~ $re ]] && [[ ${PHOTOPRISM_UID} != "0" ]] && [[ ${PHOTOPRISM_GID} =~ $re ]] && [[ ${PHOTOPRISM_GID} != "0" ]]; then
|
||||
groupadd -g "${PHOTOPRISM_GID}" "group_${PHOTOPRISM_GID}" 2>/dev/null
|
||||
useradd -o -u "${PHOTOPRISM_UID}" -g "${PHOTOPRISM_GID}" -d /photoprism "user_${PHOTOPRISM_UID}" 2>/dev/null
|
||||
usermod -g "${PHOTOPRISM_GID}" "user_${PHOTOPRISM_UID}" 2>/dev/null
|
||||
|
||||
gosu "${UID}" "$@" &
|
||||
if [[ -z ${PHOTOPRISM_DISABLE_CHOWN} ]]; then
|
||||
echo "set PHOTOPRISM_DISABLE_CHOWN: \"true\" to disable storage permission updates"
|
||||
echo "updating storage permissions..."
|
||||
chown -Rf "${PHOTOPRISM_UID}:${PHOTOPRISM_GID}" /photoprism /var/lib/photoprism /tmp/photoprism /go
|
||||
fi
|
||||
|
||||
echo "running as uid ${PHOTOPRISM_UID}:${PHOTOPRISM_GID}"
|
||||
echo "${@}"
|
||||
|
||||
gosu "${PHOTOPRISM_UID}:${PHOTOPRISM_GID}" "$@" &
|
||||
elif [[ ${PHOTOPRISM_UID} =~ $re ]] && [[ ${PHOTOPRISM_UID} != "0" ]]; then
|
||||
# User ID only
|
||||
useradd -o -u "${PHOTOPRISM_UID}" -g 1000 -d /photoprism "user_${PHOTOPRISM_UID}" 2>/dev/null
|
||||
usermod -g 1000 "user_${PHOTOPRISM_UID}" 2>/dev/null
|
||||
|
||||
if [[ -z ${PHOTOPRISM_DISABLE_CHOWN} ]]; then
|
||||
echo "set PHOTOPRISM_DISABLE_CHOWN: \"true\" to disable storage permission updates"
|
||||
echo "updating storage permissions..."
|
||||
chown -Rf "${PHOTOPRISM_UID}" /photoprism /var/lib/photoprism /tmp/photoprism /go
|
||||
fi
|
||||
|
||||
echo "running as uid ${PHOTOPRISM_UID}"
|
||||
echo "${@}"
|
||||
|
||||
gosu "${PHOTOPRISM_UID}" "$@" &
|
||||
else
|
||||
# Run as root
|
||||
echo "running as root"
|
||||
echo "${@}"
|
||||
|
||||
"$@" &
|
||||
fi
|
||||
else
|
||||
# Running as user
|
||||
echo "running as uid $(id -u)"
|
||||
echo "${@}"
|
||||
|
||||
|
@ -43,4 +93,4 @@ fi
|
|||
PID=$!
|
||||
|
||||
trap "kill $PID" INT TERM
|
||||
wait
|
||||
wait
|
||||
|
|
|
@ -45,7 +45,7 @@ services:
|
|||
- seccomp:unconfined
|
||||
- apparmor:unconfined
|
||||
# Run as a specific, non-root user (see https://docs.docker.com/engine/reference/run/#user):
|
||||
# user: "1000"
|
||||
# user: "1000:1000"
|
||||
ports:
|
||||
- "2342:2342" # [server]:[container]
|
||||
environment:
|
||||
|
@ -57,6 +57,7 @@ services:
|
|||
PHOTOPRISM_PUBLIC: "false" # No authentication required (disables password protection)
|
||||
PHOTOPRISM_READONLY: "false" # Don't modify originals directory (reduced functionality)
|
||||
PHOTOPRISM_EXPERIMENTAL: "false" # Enables experimental features
|
||||
PHOTOPRISM_DISABLE_CHOWN: "false" # Disables storage permission updates on startup
|
||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server
|
||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # Disables Settings in Web UI
|
||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # Disables all features depending on TensorFlow
|
||||
|
@ -78,14 +79,14 @@ services:
|
|||
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
|
||||
PHOTOPRISM_SITE_DESCRIPTION: ""
|
||||
PHOTOPRISM_SITE_AUTHOR: ""
|
||||
HOME: "/photoprism"
|
||||
# Set a non-root user, group, or custom umask if your Docker environment doesn't support this natively:
|
||||
# UID: 1000
|
||||
# GID: 1000
|
||||
# UMASK: 0000
|
||||
# For hardware AVC transcoding using the h264_v4l2m2m encoder:
|
||||
# PHOTOPRISM_UID: 1000
|
||||
# PHOTOPRISM_GID: 1000
|
||||
# PHOTOPRISM_UMASK: 0000
|
||||
HOME: "/photoprism"
|
||||
# Optional hardware devices for video transcoding and machine learning:
|
||||
# devices:
|
||||
# - "/dev/video11:/dev/video11"
|
||||
# - "/dev/video11:/dev/video11" # Video4Linux (h264_v4l2m2m)
|
||||
working_dir: "/photoprism"
|
||||
volumes:
|
||||
# Your photo and video files ([local path]:[container path]):
|
||||
|
|
|
@ -44,11 +44,11 @@
|
|||
"echo 'APT::Install-Suggests \"false\";' > /etc/apt/apt.conf.d/80suggests",
|
||||
"echo 'APT::Get::Assume-Yes \"true\";' > /etc/apt/apt.conf.d/80forceyes",
|
||||
"echo 'APT::Get::Fix-Missing \"true\";' > /etc/apt/apt.conf.d/80fixmissing",
|
||||
"apt-get -qqy update",
|
||||
"apt-get -qq update",
|
||||
"apt dist-upgrade 2>/dev/null",
|
||||
"apt-get -qqy install {{user `apt_packages`}}",
|
||||
"apt-get -qqy autoclean",
|
||||
"apt-get -qqy autoremove",
|
||||
"apt-get -qq install {{user `apt_packages`}}",
|
||||
"apt-get -qq autoclean",
|
||||
"apt-get -qq autoremove",
|
||||
"chmod 700 /var/lib/cloud/scripts/per-instance/install_photoprism.sh"
|
||||
]
|
||||
},
|
||||
|
|
|
@ -104,7 +104,7 @@ services:
|
|||
- seccomp:unconfined
|
||||
- apparmor:unconfined
|
||||
# Run as a specific, non-root user (see https://docs.docker.com/engine/reference/run/#user):
|
||||
user: "1000"
|
||||
user: "1000:1000"
|
||||
# Don't expose port when running behind Traefik reverse proxy!
|
||||
# ports:
|
||||
# - "2342:2342" # [server]:[container]
|
||||
|
@ -136,6 +136,7 @@ services:
|
|||
PHOTOPRISM_PUBLIC: "false" # No authentication required (disables password protection)
|
||||
PHOTOPRISM_READONLY: "false" # Don't modify originals directory (reduced functionality)
|
||||
PHOTOPRISM_EXPERIMENTAL: "false" # Enables experimental features
|
||||
PHOTOPRISM_DISABLE_CHOWN: "false" # Disables storage permission updates on startup
|
||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server
|
||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # Disables Settings in Web UI
|
||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # Disables all features depending on TensorFlow
|
||||
|
@ -149,6 +150,7 @@ services:
|
|||
PHOTOPRISM_DATABASE_NAME: "photoprism" # MariaDB database schema name
|
||||
PHOTOPRISM_DATABASE_USER: "photoprism" # MariaDB database user name
|
||||
PHOTOPRISM_DATABASE_PASSWORD: "_admin_password_" # MariaDB database user password
|
||||
PHOTOPRISM_INIT: "tensorflow-amd64-avx2"
|
||||
HOME: "/photoprism"
|
||||
working_dir: "/photoprism"
|
||||
volumes:
|
||||
|
|
|
@ -57,7 +57,7 @@ if ! command -v docker-compose &> /dev/null; then
|
|||
fi
|
||||
|
||||
# create user
|
||||
useradd photoprism -u 1000 -G docker -o -m -d /opt/photoprism || echo "User 'photoprism' already exists. Proceeding."
|
||||
useradd -o -m -U -u 1000 -G docker -d /opt/photoprism photoprism || echo "User 'photoprism' already exists. Proceeding."
|
||||
mkdir -p /opt/photoprism/originals /opt/photoprism/import /opt/photoprism/storage /opt/photoprism/backup \
|
||||
/opt/photoprism/database /opt/photoprism/traefik /opt/photoprism/certs
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ services:
|
|||
- seccomp:unconfined
|
||||
- apparmor:unconfined
|
||||
# Run as a specific, non-root user (see https://docs.docker.com/engine/reference/run/#user):
|
||||
# user: "1000"
|
||||
# user: "1000:1000"
|
||||
ports:
|
||||
- "2342:2342" # [server]:[container]
|
||||
environment:
|
||||
|
@ -56,6 +56,7 @@ services:
|
|||
PHOTOPRISM_PUBLIC: "false" # No authentication required (disables password protection)
|
||||
PHOTOPRISM_READONLY: "false" # Don't modify originals directory (reduced functionality)
|
||||
PHOTOPRISM_EXPERIMENTAL: "false" # Enables experimental features
|
||||
PHOTOPRISM_DISABLE_CHOWN: "false" # Disables storage permission updates on startup
|
||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server
|
||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # Disables Settings in Web UI
|
||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # Disables all features depending on TensorFlow
|
||||
|
@ -75,11 +76,24 @@ services:
|
|||
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
|
||||
PHOTOPRISM_SITE_DESCRIPTION: ""
|
||||
PHOTOPRISM_SITE_AUTHOR: ""
|
||||
HOME: "/photoprism"
|
||||
# Set a non-root user, group, or custom umask if your Docker environment doesn't support this natively:
|
||||
# UID: 1000
|
||||
# GID: 1000
|
||||
# UMASK: 0000
|
||||
# PHOTOPRISM_UID: 1000
|
||||
# PHOTOPRISM_GID: 1000
|
||||
# PHOTOPRISM_UMASK: 0000
|
||||
# Enable TensorFlow AVX2 support for modern Intel CPUs (requires starting the container as root):
|
||||
# PHOTOPRISM_INIT: "tensorflow-amd64-avx2"
|
||||
# Hardware video transcoding options:
|
||||
# PHOTOPRISM_FFMPEG_BUFFERS: "64" # FFmpeg capture buffers (default: 32)
|
||||
# PHOTOPRISM_FFMPEG_BITRATE: "32" # FFmpeg encoding bitrate limit in Mbit/s (default: 50)
|
||||
# PHOTOPRISM_FFMPEG_ENCODER: "h264_v4l2m2m" # Use Video4Linux for AVC transcoding (default: libx264)
|
||||
# PHOTOPRISM_FFMPEG_ENCODER: "h264_qsv" # Use Intel Quick Sync Video for AVC transcoding (default: libx264)
|
||||
# PHOTOPRISM_INIT: "intel-graphics tensorflow-amd64-avx2" # Enable TensorFlow AVX2 & Intel Graphics support
|
||||
HOME: "/photoprism"
|
||||
# Optional hardware devices for video transcoding and machine learning:
|
||||
# devices:
|
||||
# - "/dev/video11:/dev/video11" # Video4Linux (h264_v4l2m2m)
|
||||
# - "/dev/dri/renderD128:/dev/dri/renderD128" # Intel GPU
|
||||
# - "/dev/dri/card0:/dev/dri/card0"
|
||||
working_dir: "/photoprism"
|
||||
volumes:
|
||||
# Your photo and video files ([local path]:[container path]):
|
||||
|
|
|
@ -52,6 +52,7 @@ services:
|
|||
PHOTOPRISM_PUBLIC: "false" # No authentication required (disables password protection)
|
||||
PHOTOPRISM_READONLY: "false" # Don't modify originals directory (reduced functionality)
|
||||
PHOTOPRISM_EXPERIMENTAL: "false" # Enables experimental features
|
||||
PHOTOPRISM_DISABLE_CHOWN: "false" # Disables storage permission updates on startup
|
||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server
|
||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # Disables Settings in Web UI
|
||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # Disables all features depending on TensorFlow
|
||||
|
@ -70,6 +71,8 @@ services:
|
|||
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
|
||||
PHOTOPRISM_SITE_DESCRIPTION: ""
|
||||
PHOTOPRISM_SITE_AUTHOR: ""
|
||||
# Enable TensorFlow AVX2 support for modern Intel CPUs (requires starting the container as root):
|
||||
# PHOTOPRISM_INIT: "tensorflow-amd64-avx2"
|
||||
HOME: "/photoprism"
|
||||
volumes:
|
||||
# Your photo and video files ([local path]:[container path]):
|
||||
|
|
|
@ -48,7 +48,7 @@ services:
|
|||
- seccomp:unconfined
|
||||
- apparmor:unconfined
|
||||
# Run as a specific, non-root user (see https://docs.docker.com/engine/reference/run/#user):
|
||||
# user: "1000"
|
||||
# user: "1000:1000"
|
||||
ports:
|
||||
- "2342:2342" # [server]:[container]
|
||||
environment:
|
||||
|
@ -59,6 +59,7 @@ services:
|
|||
PHOTOPRISM_PUBLIC: "false" # No authentication required (disables password protection)
|
||||
PHOTOPRISM_READONLY: "false" # Don't modify originals directory (reduced functionality)
|
||||
PHOTOPRISM_EXPERIMENTAL: "false" # Enables experimental features
|
||||
PHOTOPRISM_DISABLE_CHOWN: "false" # Disables storage permission updates on startup
|
||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server
|
||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # Disables Settings in Web UI
|
||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # Disables all features depending on TensorFlow
|
||||
|
@ -78,11 +79,13 @@ services:
|
|||
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
|
||||
PHOTOPRISM_SITE_DESCRIPTION: ""
|
||||
PHOTOPRISM_SITE_AUTHOR: ""
|
||||
HOME: "/photoprism"
|
||||
# Set a non-root user, group, or custom umask if your Docker environment doesn't support this natively:
|
||||
# UID: 1000
|
||||
# GID: 1000
|
||||
# UMASK: 0000
|
||||
# PHOTOPRISM_UID: 1000
|
||||
# PHOTOPRISM_GID: 1000
|
||||
# PHOTOPRISM_UMASK: 0000
|
||||
# Enable TensorFlow AVX2 support for modern Intel CPUs (requires starting the container as root):
|
||||
# PHOTOPRISM_INIT: "tensorflow-amd64-avx2"
|
||||
HOME: "/photoprism"
|
||||
working_dir: "/photoprism"
|
||||
volumes:
|
||||
# Your photo and video files ([local path]:[container path]):
|
||||
|
|
|
@ -43,7 +43,7 @@ services:
|
|||
- seccomp:unconfined
|
||||
- apparmor:unconfined
|
||||
# Run as a specific, non-root user (see https://docs.docker.com/engine/reference/run/#user):
|
||||
# user: "1000"
|
||||
# user: "1000:1000"
|
||||
ports:
|
||||
- "2342:2342" # [server]:[container]
|
||||
environment:
|
||||
|
@ -54,6 +54,7 @@ services:
|
|||
PHOTOPRISM_PUBLIC: "false" # No authentication required (disables password protection)
|
||||
PHOTOPRISM_READONLY: "false" # Don't modify originals directory (reduced functionality)
|
||||
PHOTOPRISM_EXPERIMENTAL: "false" # Enables experimental features
|
||||
PHOTOPRISM_DISABLE_CHOWN: "false" # Disables storage permission updates on startup
|
||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server
|
||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # Disables Settings in Web UI
|
||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # Disables all features depending on TensorFlow
|
||||
|
@ -68,11 +69,13 @@ services:
|
|||
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
|
||||
PHOTOPRISM_SITE_DESCRIPTION: ""
|
||||
PHOTOPRISM_SITE_AUTHOR: ""
|
||||
HOME: "/photoprism"
|
||||
# Set a non-root user, group, or custom umask if your Docker environment doesn't support this natively:
|
||||
# UID: 1000
|
||||
# GID: 1000
|
||||
# UMASK: 0000
|
||||
# PHOTOPRISM_UID: 1000
|
||||
# PHOTOPRISM_GID: 1000
|
||||
# PHOTOPRISM_UMASK: 0000
|
||||
# Enable TensorFlow AVX2 support for modern Intel CPUs (requires starting the container as root):
|
||||
# PHOTOPRISM_INIT: "tensorflow-amd64-avx2"
|
||||
HOME: "/photoprism"
|
||||
working_dir: "/photoprism"
|
||||
volumes:
|
||||
# Your photo and video files ([local path]:[container path]):
|
||||
|
|
|
@ -55,6 +55,7 @@ services:
|
|||
PHOTOPRISM_PUBLIC: "false" # No authentication required, disables password protection
|
||||
PHOTOPRISM_READONLY: "false" # Don't modify originals folder; disables import, upload, and delete
|
||||
PHOTOPRISM_EXPERIMENTAL: "false" # Enables experimental features
|
||||
PHOTOPRISM_DISABLE_CHOWN: "false" # Disables storage permission updates on startup
|
||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server
|
||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # Disables Settings in Web UI
|
||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # Disables all features depending on TensorFlow
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM photoprism/development:20210922 as build
|
||||
FROM photoprism/development:20210929 as build
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETPLATFORM
|
||||
|
@ -31,10 +31,11 @@ RUN echo 'Acquire::Retries "10";' > /etc/apt/apt.conf.d/80retry && \
|
|||
echo 'APT::Get::Fix-Missing "true";' > /etc/apt/apt.conf.d/80fixmissing
|
||||
|
||||
# Install additional distribution packages
|
||||
RUN apt-get update && apt dist-upgrade 2>/dev/null && apt-get -qq install -y --no-install-recommends \
|
||||
RUN apt-get update && apt-get -qq dist-upgrade && apt-get -qq install --no-install-recommends \
|
||||
gpgv \
|
||||
wget \
|
||||
curl \
|
||||
make \
|
||||
davfs2 \
|
||||
ca-certificates \
|
||||
mariadb-client \
|
||||
|
@ -83,6 +84,7 @@ ENV TF_CPP_MIN_LOG_LEVEL="2" \
|
|||
PHOTOPRISM_DATABASE_NAME="photoprism" \
|
||||
PHOTOPRISM_DATABASE_USER="photoprism" \
|
||||
PHOTOPRISM_DATABASE_PASSWORD="" \
|
||||
PHOTOPRISM_DISABLE_CHOWN="false" \
|
||||
PHOTOPRISM_DISABLE_WEBDAV="false" \
|
||||
PHOTOPRISM_DISABLE_SETTINGS="false" \
|
||||
PHOTOPRISM_DISABLE_BACKUPS="false" \
|
||||
|
@ -110,13 +112,15 @@ COPY --from=build /usr/lib/libtensorflow_framework.so /usr/lib/libtensorflow_fra
|
|||
RUN ldconfig
|
||||
|
||||
# Set default umask and create photoprism user
|
||||
RUN umask 0000 && useradd photoprism -m -d /photoprism && chmod a+rwx /photoprism
|
||||
RUN umask 0000 && useradd -m -U -u 1000 -d /photoprism photoprism && chmod a+rwx /photoprism
|
||||
WORKDIR /photoprism
|
||||
|
||||
# Copy files to /photoprism
|
||||
# Copy additional files to image
|
||||
COPY --from=build /root/.local/bin/photoprism /photoprism/bin/photoprism
|
||||
COPY --from=build /root/.photoprism/assets /photoprism/assets
|
||||
COPY --chown=root:root --chmod=755 /docker/scripts/heif-convert.sh /usr/local/bin/heif-convert
|
||||
COPY --chown=root:root /docker/scripts/heif-convert.sh /usr/local/bin/heif-convert
|
||||
COPY --chown=root:root /docker/scripts/Makefile /root/Makefile
|
||||
COPY --chown=root:root /docker/photoprism/entrypoint.sh /entrypoint.sh
|
||||
|
||||
# Create directories
|
||||
RUN mkdir -m 777 -p \
|
||||
|
@ -127,7 +131,8 @@ RUN mkdir -m 777 -p \
|
|||
/photoprism/storage/config \
|
||||
/photoprism/storage/cache && \
|
||||
chown -Rf photoprism:photoprism /photoprism /var/lib/photoprism /tmp/photoprism && \
|
||||
chmod -Rf a+rwx /photoprism /var/lib/photoprism /tmp/photoprism
|
||||
chmod -Rf a+rwx /photoprism /var/lib/photoprism /tmp/photoprism && \
|
||||
chmod 755 /usr/local/bin/heif-convert /entrypoint.sh
|
||||
|
||||
# Show photoprism version
|
||||
RUN photoprism -v
|
||||
|
@ -136,7 +141,6 @@ RUN photoprism -v
|
|||
EXPOSE 2342
|
||||
|
||||
# Configure entrypoint
|
||||
COPY --chown=root:root --chmod=755 /docker/scripts/entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
VOLUME /var/lib/photoprism
|
||||
|
||||
|
|
94
docker/photoprism/entrypoint.sh
Executable file
94
docker/photoprism/entrypoint.sh
Executable file
|
@ -0,0 +1,94 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
if [[ $(id -u) == "0" ]]; then
|
||||
echo "started as root"
|
||||
|
||||
if [ -e /root/.init ]; then
|
||||
echo "initialized"
|
||||
elif [[ ${PHOTOPRISM_INIT} ]]; then
|
||||
for target in $PHOTOPRISM_INIT; do
|
||||
echo "init ${target}..."
|
||||
make -f /root/Makefile "${target}"
|
||||
done
|
||||
echo 1 >/root/.init
|
||||
fi
|
||||
fi
|
||||
|
||||
re='^[0-9]+$'
|
||||
|
||||
# Legacy umask env variable in use?
|
||||
if [[ -z ${PHOTOPRISM_UMASK} ]] && [[ ${UMASK} =~ $re ]]; then
|
||||
PHOTOPRISM_UMASK=${UMASK}
|
||||
echo "WARNING: UMASK without PHOTOPRISM_ prefix is deprecated, use PHOTOPRISM_UMASK: \"${PHOTOPRISM_UMASK}\" instead"
|
||||
fi
|
||||
|
||||
# Set file permission mask
|
||||
if [[ ${PHOTOPRISM_UMASK} =~ $re ]]; then
|
||||
echo "umask ${PHOTOPRISM_UMASK}"
|
||||
umask "${PHOTOPRISM_UMASK}"
|
||||
fi
|
||||
|
||||
# Script runs as root?
|
||||
if [[ $(id -u) == "0" ]]; then
|
||||
# Legacy user ID env variable in use?
|
||||
if [[ -z ${PHOTOPRISM_UID} ]] && [[ ${UID} =~ $re ]] && [[ ${UID} != "0" ]]; then
|
||||
PHOTOPRISM_UID=${UID}
|
||||
echo "WARNING: UID without PHOTOPRISM_ prefix is deprecated, use PHOTOPRISM_UID: \"${PHOTOPRISM_UID}\" instead"
|
||||
fi
|
||||
|
||||
# Legacy group ID env variable in use?
|
||||
if [[ -z ${PHOTOPRISM_GID} ]] && [[ ${GID} =~ $re ]] && [[ ${GID} != "0" ]]; then
|
||||
PHOTOPRISM_GID=${GID}
|
||||
echo "WARNING: GID without PHOTOPRISM_ prefix is deprecated, use PHOTOPRISM_GID: \"${PHOTOPRISM_GID}\" instead"
|
||||
fi
|
||||
|
||||
# User and group ID set?
|
||||
if [[ ${PHOTOPRISM_UID} =~ $re ]] && [[ ${PHOTOPRISM_UID} != "0" ]] && [[ ${PHOTOPRISM_GID} =~ $re ]] && [[ ${PHOTOPRISM_GID} != "0" ]]; then
|
||||
groupadd -g "${PHOTOPRISM_GID}" "group_${PHOTOPRISM_GID}" 2>/dev/null
|
||||
useradd -o -u "${PHOTOPRISM_UID}" -g "${PHOTOPRISM_GID}" -d /photoprism "user_${PHOTOPRISM_UID}" 2>/dev/null
|
||||
usermod -g "${PHOTOPRISM_GID}" "user_${PHOTOPRISM_UID}" 2>/dev/null
|
||||
|
||||
if [[ -z ${PHOTOPRISM_DISABLE_CHOWN} ]]; then
|
||||
echo "set PHOTOPRISM_DISABLE_CHOWN: \"true\" to disable storage permission updates"
|
||||
echo "updating storage permissions..."
|
||||
chown -Rf "${PHOTOPRISM_UID}:${PHOTOPRISM_GID}" /photoprism/storage /photoprism/import /photoprism/assets /var/lib/photoprism /tmp/photoprism
|
||||
fi
|
||||
|
||||
echo "running as uid ${PHOTOPRISM_UID}:${PHOTOPRISM_GID}"
|
||||
echo "${@}"
|
||||
|
||||
gosu "${PHOTOPRISM_UID}:${PHOTOPRISM_GID}" "$@" &
|
||||
elif [[ ${PHOTOPRISM_UID} =~ $re ]] && [[ ${PHOTOPRISM_UID} != "0" ]]; then
|
||||
# User ID only
|
||||
useradd -o -u "${PHOTOPRISM_UID}" -g 1000 -d /photoprism "user_${PHOTOPRISM_UID}" 2>/dev/null
|
||||
usermod -g 1000 "user_${PHOTOPRISM_UID}" 2>/dev/null
|
||||
|
||||
if [[ -z ${PHOTOPRISM_DISABLE_CHOWN} ]]; then
|
||||
echo "set PHOTOPRISM_DISABLE_CHOWN: \"true\" to disable storage permission updates"
|
||||
echo "updating storage permissions..."
|
||||
chown -Rf "${PHOTOPRISM_UID}" /photoprism/storage /photoprism/import /photoprism/assets /var/lib/photoprism /tmp/photoprism
|
||||
fi
|
||||
|
||||
echo "running as uid ${PHOTOPRISM_UID}"
|
||||
echo "${@}"
|
||||
|
||||
gosu "${PHOTOPRISM_UID}" "$@" &
|
||||
else
|
||||
# No user or group ID set via end variable
|
||||
echo "running as root"
|
||||
echo "${@}"
|
||||
|
||||
"$@" &
|
||||
fi
|
||||
else
|
||||
# Running as root
|
||||
echo "running as uid $(id -u)"
|
||||
echo "${@}"
|
||||
|
||||
"$@" &
|
||||
fi
|
||||
|
||||
PID=$!
|
||||
|
||||
trap "kill $PID" INT TERM
|
||||
wait
|
29
docker/scripts/Makefile
Normal file
29
docker/scripts/Makefile
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Optional packages and drivers for PhotoPrism
|
||||
# Maintainer: Michael Mayer <hello@photoprism.org>
|
||||
.PHONY: tensorflow-amd64-cpu tensorflow-amd64-cpu-install \
|
||||
tensorflow-amd64-avx tensorflow-amd64-avx-install \
|
||||
tensorflow-amd64-avx2 tensorflow-amd64-avx2-install \
|
||||
intel-graphics;
|
||||
tensorflow-amd64-cpu: /tmp/libtensorflow-linux-cpu-1.15.2.tar.gz tensorflow-amd64-cpu-install
|
||||
/tmp/libtensorflow-linux-cpu-1.15.2.tar.gz:
|
||||
curl -fsSL "https://dl.photoprism.org/tensorflow/linux/libtensorflow-linux-cpu-1.15.2.tar.gz" > /tmp/libtensorflow-linux-cpu-1.15.2.tar.gz
|
||||
tensorflow-amd64-cpu-install:
|
||||
tar --overwrite -C "/usr" -xzf /tmp/libtensorflow-linux-cpu-1.15.2.tar.gz
|
||||
ldconfig
|
||||
tensorflow-amd64-avx: /tmp/libtensorflow-linux-avx-1.15.2.tar.gz tensorflow-amd64-avx-install
|
||||
/tmp/libtensorflow-linux-avx-1.15.2.tar.gz:
|
||||
curl -fsSL "https://dl.photoprism.org/tensorflow/linux/libtensorflow-linux-avx-1.15.2.tar.gz" > /tmp/libtensorflow-linux-avx-1.15.2.tar.gz
|
||||
tensorflow-amd64-avx-install:
|
||||
tar --overwrite -C "/usr" -xzf /tmp/libtensorflow-linux-avx-1.15.2.tar.gz
|
||||
ldconfig
|
||||
tensorflow-amd64-avx2: /tmp/libtensorflow-linux-avx2-1.15.2.tar.gz tensorflow-amd64-avx2-install
|
||||
/tmp/libtensorflow-linux-avx2-1.15.2.tar.gz:
|
||||
curl -fsSL "https://dl.photoprism.org/tensorflow/linux/libtensorflow-linux-avx2-1.15.2.tar.gz" > /tmp/libtensorflow-linux-avx2-1.15.2.tar.gz
|
||||
tensorflow-amd64-avx2-install:
|
||||
tar --overwrite -C "/usr" -xzf /tmp/libtensorflow-linux-avx2-1.15.2.tar.gz
|
||||
ldconfig
|
||||
intel-graphics:
|
||||
apt-get update
|
||||
apt-get dist-upgrade
|
||||
apt-get install intel-opencl-icd intel-media-va-driver-non-free i965-va-driver-shaders libmfx1
|
||||
apt-get -y autoremove && apt-get -y autoclean && apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
@ -1,43 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
if [[ ${UMASK} ]]; then
|
||||
echo "umask ${UMASK}"
|
||||
umask "${UMASK}"
|
||||
fi
|
||||
|
||||
if [[ ${UID} ]] && [[ ${GID} ]] && [[ ${UID} != "0" ]] && [[ $(id -u) = "0" ]]; then
|
||||
groupadd -f -g "${GID}" "${GID}"
|
||||
usermod -o -u "${UID}" -g "${GID}" photoprism
|
||||
|
||||
if [[ -z ${PHOTOPRISM_DISABLE_CHOWN} ]] ; then
|
||||
echo "updating storage permissions..."
|
||||
chown -Rf photoprism /photoprism/storage /photoprism/import /photoprism/assets /var/lib/photoprism /tmp/photoprism
|
||||
fi
|
||||
|
||||
echo "running as uid ${UID}:${GID}"
|
||||
echo "${@}"
|
||||
|
||||
gosu "${UID}:${GID}" "$@" &
|
||||
elif [[ ${UID} ]] && [[ ${UID} != "0" ]] && [[ $(id -u) = "0" ]]; then
|
||||
usermod -o -u "${UID}" photoprism
|
||||
|
||||
if [[ -z ${PHOTOPRISM_DISABLE_CHOWN} ]] ; then
|
||||
echo "updating storage permissions..."
|
||||
chown -Rf photoprism /photoprism/storage /photoprism/import /photoprism/assets /var/lib/photoprism /tmp/photoprism
|
||||
fi
|
||||
|
||||
echo "running as uid ${UID}"
|
||||
echo "${@}"
|
||||
|
||||
gosu "${UID}" "$@" &
|
||||
else
|
||||
echo "running as uid $(id -u)"
|
||||
echo "${@}"
|
||||
|
||||
"$@" &
|
||||
fi
|
||||
|
||||
PID=$!
|
||||
|
||||
trap "kill $PID" INT TERM
|
||||
wait
|
|
@ -12,14 +12,14 @@ else
|
|||
elif [[ $1 == "arm64" ]]; then
|
||||
URL="https://dl.photoprism.org/tensorflow/$1/libtensorflow-$1-1.15.2.tar.gz"
|
||||
elif [[ $1 == "arm" ]]; then
|
||||
URL="https://github.com/Qengineering/TensorFlow-Raspberry-Pi/blob/master/libtensorflow_1_15_2.tar.gz?raw=true"
|
||||
URL="https://dl.photoprism.org/tensorflow/$1/libtensorflow-$1-1.15.2.tar.gz"
|
||||
else
|
||||
echo "cpu architecture not supported by now" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$URL"
|
||||
curl -L "$URL" | \
|
||||
tar -C "/usr" -xz && \
|
||||
curl -fsSL "$URL" | \
|
||||
tar --overwrite -C "/usr" -xz && \
|
||||
ldconfig
|
||||
echo "Done"
|
||||
fi
|
||||
|
|
2352
frontend/package-lock.json
generated
2352
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -188,6 +188,24 @@ export default class Config {
|
|||
}
|
||||
}
|
||||
|
||||
getPerson(name) {
|
||||
name = name.toLowerCase();
|
||||
|
||||
const result = this.values.people.filter((m) => m.Name.toLowerCase() === name);
|
||||
const l = result ? result.length : 0;
|
||||
|
||||
if (l === 0) {
|
||||
return null;
|
||||
} else if (l === 1) {
|
||||
return result[0];
|
||||
} else {
|
||||
if (this.debug) {
|
||||
console.warn("more than one person matching the same name", result);
|
||||
}
|
||||
return result[0];
|
||||
}
|
||||
}
|
||||
|
||||
onCount(ev, data) {
|
||||
const type = ev.split(".")[1];
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<v-layout row wrap class="search-results photo-results cards-view" :class="{'select-results': selectMode}">
|
||||
<v-flex
|
||||
v-for="(photo, index) in photos"
|
||||
:key="index"
|
||||
:key="photo.ID"
|
||||
xs12 sm6 md4 lg3 xlg2 xxxl1 d-flex
|
||||
>
|
||||
<v-card tile
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<v-layout row wrap class="search-results photo-results mosaic-view" :class="{'select-results': selectMode}">
|
||||
<v-flex
|
||||
v-for="(photo, index) in photos"
|
||||
:key="index"
|
||||
:key="photo.ID"
|
||||
xs4 sm3 md2 lg1 d-flex
|
||||
>
|
||||
<v-card tile
|
||||
|
|
|
@ -202,6 +202,8 @@ main {
|
|||
#photoprism .v-tabs .v-badge__badge {
|
||||
right: -22px;
|
||||
font-size: 9px;
|
||||
width: 1.1rem;
|
||||
width: auto;
|
||||
padding: 0 0.2rem;
|
||||
min-width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
}
|
|
@ -39,6 +39,7 @@ import PFileDeleteDialog from "./file/delete.vue";
|
|||
import PAlbumEditDialog from "./album/edit.vue";
|
||||
import PAlbumDeleteDialog from "./album/delete.vue";
|
||||
import PLabelDeleteDialog from "./label/delete.vue";
|
||||
import PPeopleMergeDialog from "./people/merge.vue";
|
||||
import PUploadDialog from "./upload.vue";
|
||||
import PVideoViewer from "./video/viewer.vue";
|
||||
import PShareDialog from "./share.vue";
|
||||
|
@ -61,6 +62,7 @@ dialogs.install = (Vue) => {
|
|||
Vue.component("PAlbumEditDialog", PAlbumEditDialog);
|
||||
Vue.component("PAlbumDeleteDialog", PAlbumDeleteDialog);
|
||||
Vue.component("PLabelDeleteDialog", PLabelDeleteDialog);
|
||||
Vue.component("PPeopleMergeDialog", PPeopleMergeDialog);
|
||||
Vue.component("PUploadDialog", PUploadDialog);
|
||||
Vue.component("PVideoViewer", PVideoViewer);
|
||||
Vue.component("PShareDialog", PShareDialog);
|
||||
|
|
65
frontend/src/dialog/people/merge.vue
Normal file
65
frontend/src/dialog/people/merge.vue
Normal file
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<v-dialog v-model="show" lazy persistent max-width="350" class="p-people-merge-dialog" @keydown.esc="cancel">
|
||||
<v-card raised elevation="24">
|
||||
<v-container fluid class="pb-2 pr-2 pl-2">
|
||||
<v-layout row wrap>
|
||||
<v-flex xs3 text-xs-center>
|
||||
<v-icon size="54" color="secondary-dark lighten-1">people</v-icon>
|
||||
</v-flex>
|
||||
<v-flex xs9 text-xs-left align-self-center>
|
||||
<div class="subheading pr-1">
|
||||
{{ prompt }}
|
||||
</div>
|
||||
</v-flex>
|
||||
<v-flex xs12 text-xs-right class="pt-3">
|
||||
<v-btn depressed color="secondary-light" class="action-cancel" @click.stop="cancel">
|
||||
<translate key="No">No</translate>
|
||||
</v-btn>
|
||||
<v-btn color="primary-button" depressed dark class="action-confirm"
|
||||
@click.stop="confirm">
|
||||
<translate key="Yes">Yes</translate>
|
||||
</v-btn>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
import Subject from "model/subject";
|
||||
|
||||
export default {
|
||||
name: 'PPeopleMergeDialog',
|
||||
props: {
|
||||
show: Boolean,
|
||||
subj1: {
|
||||
type: Object,
|
||||
default: new Subject(),
|
||||
},
|
||||
subj2: {
|
||||
type: Object,
|
||||
default: new Subject(),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
prompt() {
|
||||
if (!this.subj1 || !this.subj2) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return this.$gettextInterpolate(this.$gettext("Merge %{a} with %{b}?"), {a: this.subj1.originalValue("Name"), b: this.subj2.Name});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
confirm() {
|
||||
this.$emit('confirm');
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div class="p-tab p-tab-photo-files">
|
||||
<v-expansion-panel expand class="pa-0 elevation-0 secondary" :value="state">
|
||||
<template v-for="(file, index) in model.fileModels()">
|
||||
<v-expansion-panel-content v-if="!file.Missing" :key="index" class="pa-0 elevation-0 secondary-light"
|
||||
<template v-for="file in model.fileModels()">
|
||||
<v-expansion-panel-content v-if="!file.Missing" :key="file.UID" class="pa-0 elevation-0 secondary-light"
|
||||
style="margin-top: 1px;">
|
||||
<template #header>
|
||||
<div class="caption">{{ file.baseName(70) }}</div>
|
||||
|
@ -41,13 +41,13 @@
|
|||
@click.stop.prevent="downloadFile(file)">
|
||||
<translate>Download</translate>
|
||||
</v-btn>
|
||||
<v-btn v-if="features.edit && file.Type === 'jpg' && !file.Primary" small depressed dark
|
||||
<v-btn v-if="features.edit && file.Type === 'jpg' && file.Error === '' && !file.Primary" small depressed dark
|
||||
color="primary-button"
|
||||
class="ma-0 action-primary"
|
||||
@click.stop.prevent="primaryFile(file)">
|
||||
<translate>Primary</translate>
|
||||
</v-btn>
|
||||
<v-btn v-if="features.edit && !file.Sidecar && !file.Primary && file.Root === '/'" small
|
||||
<v-btn v-if="features.edit && !file.Sidecar && file.Error === '' && !file.Primary && file.Root === '/'" small
|
||||
depressed dark color="primary-button"
|
||||
class="ma-0 action-unstack"
|
||||
@click.stop.prevent="unstackFile(file)">
|
||||
|
@ -66,6 +66,12 @@
|
|||
</td>
|
||||
<td>{{ file.UID | uppercase }}</td>
|
||||
</tr>
|
||||
<tr v-if="file.Error">
|
||||
<td>
|
||||
<translate>Error</translate>
|
||||
</td>
|
||||
<td><span class="body-2">{{ file.Error | uppercase }}</span></td>
|
||||
</tr>
|
||||
<tr v-if="file.InstanceID" title="XMP">
|
||||
<td>
|
||||
<translate>Instance ID</translate>
|
||||
|
@ -162,12 +168,6 @@
|
|||
</td>
|
||||
<td>{{ file.Chroma }} / 100</td>
|
||||
</tr>
|
||||
<tr v-if="file.Error">
|
||||
<td>
|
||||
<translate>Error</translate>
|
||||
</td>
|
||||
<td>{{ file.Error }}</td>
|
||||
</tr>
|
||||
<tr v-if="file.Missing">
|
||||
<td>
|
||||
<translate>Missing</translate>
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
v-model="marker.Name"
|
||||
:rules="[textRule]"
|
||||
:disabled="busy"
|
||||
:readonly="true"
|
||||
browser-autocomplete="off"
|
||||
class="input-name pa-0 ma-0"
|
||||
hide-details
|
||||
|
@ -132,7 +133,11 @@ export default {
|
|||
},
|
||||
onReject(marker) {
|
||||
this.busy = true;
|
||||
marker.reject().finally(() => this.busy = false);
|
||||
this.$notify.blockUI();
|
||||
marker.reject().finally(() => {
|
||||
this.$notify.unblockUI();
|
||||
this.busy = false;
|
||||
});
|
||||
},
|
||||
onApprove(marker) {
|
||||
this.busy = true;
|
||||
|
@ -140,11 +145,19 @@ export default {
|
|||
},
|
||||
onClearSubject(marker) {
|
||||
this.busy = true;
|
||||
marker.clearSubject(marker).finally(() => this.busy = false);
|
||||
this.$notify.blockUI();
|
||||
marker.clearSubject(marker).finally(() => {
|
||||
this.$notify.unblockUI();
|
||||
this.busy = false;
|
||||
});
|
||||
},
|
||||
onRename(marker) {
|
||||
this.busy = true;
|
||||
marker.rename().finally(() => this.busy = false);
|
||||
this.$notify.blockUI();
|
||||
marker.rename().finally(() => {
|
||||
this.$notify.unblockUI();
|
||||
this.busy = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-expansion-panel class="pa-0 elevation-0">
|
||||
<v-expansion-panel-content v-for="(link, index) in links" :key="index"
|
||||
<v-expansion-panel-content v-for="(link, index) in links" :key="link.UID"
|
||||
class="pa-0 elevation-0 secondary mb-1">
|
||||
<template #header>
|
||||
<button :class="`text-xs-${!rtl ? 'left' : 'right'} action-url ml-0 mt-0 mb-0 pa-0 mr-2`" style="user-select: none;"
|
||||
|
|
Binary file not shown.
|
@ -37,7 +37,7 @@ msgstr "%{n} Ordner gefunden"
|
|||
msgid "%{n} labels found"
|
||||
msgstr "%{n} Kategorien gefunden"
|
||||
|
||||
#: src/pages/people/faces.vue:357 src/pages/people/subjects.vue:348
|
||||
#: src/pages/people/faces.vue:364 src/pages/people/subjects.vue:389
|
||||
msgid "%{n} people found"
|
||||
msgstr "%{n} Personen gefunden"
|
||||
|
||||
|
@ -184,7 +184,7 @@ msgstr "Alle %{n} Einträge werden angezeigt"
|
|||
msgid "All %{n} labels loaded"
|
||||
msgstr "Alle %{n} Kategorien werden angezeigt"
|
||||
|
||||
#: src/pages/people/faces.vue:264 src/pages/people/subjects.vue:255
|
||||
#: src/pages/people/faces.vue:271 src/pages/people/subjects.vue:296
|
||||
msgid "All %{n} people loaded"
|
||||
msgstr "Alle %{n} Personen geladen"
|
||||
|
||||
|
@ -330,7 +330,7 @@ msgstr ""
|
|||
msgid "Automatically creates albums of special moments, trips, and places."
|
||||
msgstr "Erstellt automatisch Alben mit besonderen Momenten, Reisen und Orten."
|
||||
|
||||
#: src/pages/people/subjects.vue:351
|
||||
#: src/pages/people/subjects.vue:352
|
||||
msgid "Bio"
|
||||
msgstr "Biographie"
|
||||
|
||||
|
@ -393,9 +393,9 @@ msgstr "Limit erreicht, bitte Suche eingrenzen"
|
|||
#: src/common/clipboard.js:105 src/common/clipboard.js:142
|
||||
#: src/pages/albums.vue:387 src/pages/albums.vue:403 src/pages/labels.vue:182
|
||||
#: src/pages/labels.vue:198 src/pages/library/files.vue:193
|
||||
#: src/pages/library/files.vue:209 src/pages/people/faces.vue:200
|
||||
#: src/pages/people/faces.vue:216 src/pages/people/subjects.vue:191
|
||||
#: src/pages/people/subjects.vue:207 src/share/albums.vue:345
|
||||
#: src/pages/library/files.vue:209 src/pages/people/faces.vue:207
|
||||
#: src/pages/people/faces.vue:223 src/pages/people/subjects.vue:232
|
||||
#: src/pages/people/subjects.vue:248 src/share/albums.vue:345
|
||||
#: src/share/albums.vue:361
|
||||
msgid "Can't select more items"
|
||||
msgstr "Maximale Anzahl wurde selektiert"
|
||||
|
@ -1302,6 +1302,10 @@ msgstr "Bildbereich"
|
|||
msgid "Medium"
|
||||
msgstr "Mittel"
|
||||
|
||||
#: src/dialog/people/merge.vue:25
|
||||
msgid "Merge %{a} with %{b}?"
|
||||
msgstr "%{a} und %{b} zusammenfassen?"
|
||||
|
||||
#: src/pages/about/feedback.vue:27
|
||||
msgid "Message sent"
|
||||
msgstr "Nachricht versendet"
|
||||
|
@ -1335,7 +1339,7 @@ msgstr "Moonlight"
|
|||
msgid "More than 20 albums found"
|
||||
msgstr "Mehr als 20 Alben gefunden"
|
||||
|
||||
#: src/pages/people/faces.vue:360
|
||||
#: src/pages/people/faces.vue:367
|
||||
msgid "More than 20 faces found"
|
||||
msgstr "Mehr als 20 Gesichter gefunden"
|
||||
|
||||
|
@ -1343,7 +1347,7 @@ msgstr "Mehr als 20 Gesichter gefunden"
|
|||
msgid "More than 20 labels found"
|
||||
msgstr "Mehr als 20 Labels gefunden"
|
||||
|
||||
#: src/pages/people/subjects.vue:351
|
||||
#: src/pages/people/subjects.vue:392
|
||||
msgid "More than 20 people found"
|
||||
msgstr "Mehr als 20 Personen gefunden"
|
||||
|
||||
|
@ -1373,19 +1377,19 @@ msgstr "Dateien verschieben"
|
|||
#: src/dialog/album/edit.vue:106 src/dialog/photo/files.vue:70
|
||||
#: src/dialog/photo/files.vue:67 src/dialog/photo/files.vue:30
|
||||
#: src/dialog/photo/info.vue:31 src/dialog/photo/labels.vue:48
|
||||
#: src/dialog/photo/people.vue:18 src/dialog/photo/people.vue:228
|
||||
#: src/dialog/photo/people.vue:19 src/dialog/photo/people.vue:229
|
||||
#: src/pages/about/feedback.vue:144 src/pages/labels.vue:337
|
||||
#: src/pages/login.vue:73 src/pages/people/faces.vue:46
|
||||
#: src/pages/people/faces.vue:312 src/pages/people/subjects.vue:314
|
||||
#: src/pages/login.vue:73 src/pages/people/faces.vue:47
|
||||
#: src/pages/people/faces.vue:310 src/pages/people/subjects.vue:315
|
||||
#: src/share/photo/cards.vue:30 src/share/photo/list.vue:34
|
||||
#: src/share/photo/list.vue:216
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: src/component/album/toolbar.vue:52 src/dialog/album/edit.vue:24
|
||||
#: src/dialog/photo/labels.vue:22 src/dialog/photo/people.vue:21
|
||||
#: src/dialog/photo/labels.vue:22 src/dialog/photo/people.vue:22
|
||||
#: src/pages/labels.vue:38 src/pages/library/files.vue:40
|
||||
#: src/pages/people/faces.vue:40 src/pages/people/subjects.vue:39
|
||||
#: src/pages/people/faces.vue:41 src/pages/people/subjects.vue:40
|
||||
msgid "Name too long"
|
||||
msgstr "Name zu lang"
|
||||
|
||||
|
@ -1407,9 +1411,10 @@ msgstr "Neues Passwort"
|
|||
msgid "Newest first"
|
||||
msgstr "Neueste zuerst"
|
||||
|
||||
#: src/dialog/photo/archive.vue:15 src/dialog/photo/info.vue:284
|
||||
#: src/dialog/photo/info.vue:305 src/dialog/photo/info.vue:325
|
||||
#: src/dialog/photo/info.vue:345 src/dialog/photo/info.vue:365
|
||||
#: src/dialog/people/merge.vue:15 src/dialog/photo/archive.vue:15
|
||||
#: src/dialog/photo/info.vue:284 src/dialog/photo/info.vue:305
|
||||
#: src/dialog/photo/info.vue:325 src/dialog/photo/info.vue:345
|
||||
#: src/dialog/photo/info.vue:365
|
||||
msgid "No"
|
||||
msgstr "Nein"
|
||||
|
||||
|
@ -1428,8 +1433,8 @@ msgid "No labels found"
|
|||
msgstr "Keine Kategorien gefunden"
|
||||
|
||||
#: src/dialog/photo/people.vue:5 src/pages/people/faces.vue:23
|
||||
#: src/pages/people/faces.vue:353 src/pages/people/subjects.vue:26
|
||||
#: src/pages/people/subjects.vue:344
|
||||
#: src/pages/people/faces.vue:360 src/pages/people/subjects.vue:26
|
||||
#: src/pages/people/subjects.vue:385
|
||||
msgid "No people found"
|
||||
msgstr "Keine Personen gefunden"
|
||||
|
||||
|
@ -1550,7 +1555,7 @@ msgstr "Ein Ordner gefunden"
|
|||
msgid "One label found"
|
||||
msgstr "Eine Kategorie gefunden"
|
||||
|
||||
#: src/pages/people/faces.vue:355 src/pages/people/subjects.vue:346
|
||||
#: src/pages/people/faces.vue:362 src/pages/people/subjects.vue:387
|
||||
msgid "One person found"
|
||||
msgstr "Eine Person gefunden"
|
||||
|
||||
|
@ -1826,7 +1831,7 @@ msgstr "Rot"
|
|||
#: src/component/album/toolbar.vue:54 src/component/photo/toolbar.vue:57
|
||||
#: src/dialog/reload.vue:15 src/pages/albums.vue:134 src/pages/labels.vue:91
|
||||
#: src/pages/library/errors.vue:69 src/pages/library/files.vue:100
|
||||
#: src/pages/people/faces.vue:67 src/pages/people/subjects.vue:97
|
||||
#: src/pages/people/faces.vue:67 src/pages/people/subjects.vue:98
|
||||
msgid "Reload"
|
||||
msgstr "Neu laden"
|
||||
|
||||
|
@ -1909,7 +1914,7 @@ msgstr "Scans"
|
|||
#: src/component/navigation.vue:4 src/component/navigation.vue:280
|
||||
#: src/component/photo/toolbar.vue:48 src/pages/albums.vue:109
|
||||
#: src/pages/labels.vue:81 src/pages/library/errors.vue:60
|
||||
#: src/pages/people/subjects.vue:80 src/pages/places.vue:30 src/routes.js:107
|
||||
#: src/pages/people/subjects.vue:81 src/pages/places.vue:30 src/routes.js:107
|
||||
msgid "Search"
|
||||
msgstr "Suche"
|
||||
|
||||
|
@ -2166,7 +2171,7 @@ msgstr "Aufgenommen"
|
|||
msgid "Teal"
|
||||
msgstr "Blaugrün"
|
||||
|
||||
#: src/dialog/photo/details.vue:26 src/pages/people/faces.vue:49
|
||||
#: src/dialog/photo/details.vue:26 src/pages/people/faces.vue:50
|
||||
msgid "Text too long"
|
||||
msgstr "Text ist zu lang"
|
||||
|
||||
|
@ -2446,12 +2451,13 @@ msgstr "Gelb"
|
|||
msgid "Yellowstone"
|
||||
msgstr "Yellowstone"
|
||||
|
||||
#: src/dialog/photo/archive.vue:18 src/dialog/photo/files.vue:103
|
||||
#: src/dialog/photo/files.vue:111 src/dialog/photo/files.vue:157
|
||||
#: src/dialog/photo/files.vue:100 src/dialog/photo/files.vue:108
|
||||
#: src/dialog/photo/files.vue:154 src/dialog/photo/info.vue:284
|
||||
#: src/dialog/photo/info.vue:305 src/dialog/photo/info.vue:325
|
||||
#: src/dialog/photo/info.vue:345 src/dialog/photo/info.vue:365
|
||||
#: src/dialog/people/merge.vue:18 src/dialog/photo/archive.vue:18
|
||||
#: src/dialog/photo/files.vue:103 src/dialog/photo/files.vue:111
|
||||
#: src/dialog/photo/files.vue:157 src/dialog/photo/files.vue:100
|
||||
#: src/dialog/photo/files.vue:108 src/dialog/photo/files.vue:154
|
||||
#: src/dialog/photo/info.vue:284 src/dialog/photo/info.vue:305
|
||||
#: src/dialog/photo/info.vue:325 src/dialog/photo/info.vue:345
|
||||
#: src/dialog/photo/info.vue:365
|
||||
msgid "Yes"
|
||||
msgstr "Ja"
|
||||
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
@ -9,8 +9,8 @@ msgstr ""
|
|||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/files.vue:131
|
||||
#: src/dialog/photo/files.vue:128
|
||||
#: src/dialog/photo/files.vue:137
|
||||
#: src/dialog/photo/files.vue:134
|
||||
msgid "{{ file.Orientation }}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -36,8 +36,9 @@ msgstr ""
|
|||
msgid "%{n} labels found"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/people/faces.vue:357
|
||||
#: src/pages/people/subjects.vue:348
|
||||
#: src/pages/people/faces.vue:267
|
||||
#: src/pages/people/faces.vue:353
|
||||
#: src/pages/people/subjects.vue:392
|
||||
msgid "%{n} people found"
|
||||
msgstr ""
|
||||
|
||||
|
@ -161,7 +162,7 @@ msgstr ""
|
|||
msgid "After two weeks"
|
||||
msgstr ""
|
||||
|
||||
#: src/model/album.js:189
|
||||
#: src/model/album.js:179
|
||||
msgid "Album"
|
||||
msgstr ""
|
||||
|
||||
|
@ -198,8 +199,7 @@ msgstr ""
|
|||
msgid "All %{n} labels loaded"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/people/faces.vue:264
|
||||
#: src/pages/people/subjects.vue:255
|
||||
#: src/pages/people/subjects.vue:295
|
||||
msgid "All %{n} people loaded"
|
||||
msgstr ""
|
||||
|
||||
|
@ -329,8 +329,8 @@ msgstr ""
|
|||
msgid "Artist"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/files.vue:122
|
||||
#: src/dialog/photo/files.vue:119
|
||||
#: src/dialog/photo/files.vue:128
|
||||
#: src/dialog/photo/files.vue:125
|
||||
msgid "Aspect Ratio"
|
||||
msgstr ""
|
||||
|
||||
|
@ -346,7 +346,7 @@ msgstr ""
|
|||
msgid "Automatically creates albums of special moments, trips, and places."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/people/subjects.vue:351
|
||||
#: src/pages/people/subjects.vue:352
|
||||
msgid "Bio"
|
||||
msgstr ""
|
||||
|
||||
|
@ -420,10 +420,10 @@ msgstr ""
|
|||
#: src/pages/labels.vue:198
|
||||
#: src/pages/library/files.vue:193
|
||||
#: src/pages/library/files.vue:209
|
||||
#: src/pages/people/faces.vue:200
|
||||
#: src/pages/people/faces.vue:216
|
||||
#: src/pages/people/subjects.vue:191
|
||||
#: src/pages/people/subjects.vue:207
|
||||
#: src/pages/people/faces.vue:196
|
||||
#: src/pages/people/faces.vue:212
|
||||
#: src/pages/people/subjects.vue:229
|
||||
#: src/pages/people/subjects.vue:245
|
||||
#: src/share/albums.vue:345
|
||||
#: src/share/albums.vue:361
|
||||
msgid "Can't select more items"
|
||||
|
@ -482,8 +482,8 @@ msgstr ""
|
|||
msgid "Chinese Traditional"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/files.vue:142
|
||||
#: src/dialog/photo/files.vue:139
|
||||
#: src/dialog/photo/files.vue:148
|
||||
#: src/dialog/photo/files.vue:145
|
||||
msgid "Chroma"
|
||||
msgstr ""
|
||||
|
||||
|
@ -494,8 +494,8 @@ msgstr ""
|
|||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/files.vue:94
|
||||
#: src/dialog/photo/files.vue:91
|
||||
#: src/dialog/photo/files.vue:100
|
||||
#: src/dialog/photo/files.vue:97
|
||||
msgid "Codec"
|
||||
msgstr ""
|
||||
|
||||
|
@ -733,10 +733,10 @@ msgstr ""
|
|||
msgid "Disables reverse geocoding and maps."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes.js:361
|
||||
#: src/routes.js:368
|
||||
#: src/routes.js:375
|
||||
#: src/routes.js:382
|
||||
#: src/routes.js:389
|
||||
#: src/routes.js:396
|
||||
msgid "Discover"
|
||||
msgstr ""
|
||||
|
||||
|
@ -894,8 +894,8 @@ msgstr ""
|
|||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/files.vue:148
|
||||
#: src/dialog/photo/files.vue:145
|
||||
#: src/dialog/photo/files.vue:52
|
||||
#: src/dialog/photo/files.vue:49
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
|
@ -944,7 +944,7 @@ msgstr ""
|
|||
msgid "F Number"
|
||||
msgstr ""
|
||||
|
||||
#: src/model/face.js:115
|
||||
#: src/model/face.js:153
|
||||
msgid "Face"
|
||||
msgstr ""
|
||||
|
||||
|
@ -992,7 +992,7 @@ msgstr ""
|
|||
msgid "Feedback"
|
||||
msgstr ""
|
||||
|
||||
#: src/model/file.js:245
|
||||
#: src/model/file.js:244
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1076,8 +1076,8 @@ msgstr ""
|
|||
msgid "Group by similarity"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/files.vue:58
|
||||
#: src/dialog/photo/files.vue:55
|
||||
#: src/dialog/photo/files.vue:64
|
||||
#: src/dialog/photo/files.vue:61
|
||||
msgid "Hash"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1097,7 +1097,7 @@ msgstr ""
|
|||
msgid "Hidden Files"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/people/faces.vue:192
|
||||
#: src/pages/people/faces.vue:186
|
||||
msgid "Hide"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1184,8 +1184,8 @@ msgstr ""
|
|||
msgid "Indexing media and sidecar files…"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/files.vue:52
|
||||
#: src/dialog/photo/files.vue:49
|
||||
#: src/dialog/photo/files.vue:58
|
||||
#: src/dialog/photo/files.vue:55
|
||||
msgid "Instance ID"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1287,9 +1287,9 @@ msgstr ""
|
|||
#: src/component/navigation.vue:1131
|
||||
#: src/pages/settings.vue:41
|
||||
#: src/pages/settings/general.vue:404
|
||||
#: src/routes.js:279
|
||||
#: src/routes.js:286
|
||||
#: src/routes.js:293
|
||||
#: src/routes.js:300
|
||||
#: src/routes.js:307
|
||||
msgid "Library"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1381,8 +1381,8 @@ msgstr ""
|
|||
msgid "Magenta"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/files.vue:136
|
||||
#: src/dialog/photo/files.vue:133
|
||||
#: src/dialog/photo/files.vue:142
|
||||
#: src/dialog/photo/files.vue:139
|
||||
msgid "Main Color"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1402,6 +1402,10 @@ msgstr ""
|
|||
msgid "Medium"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/people/merge.vue:25
|
||||
msgid "Merge %{a} with %{b}?"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/about/feedback.vue:27
|
||||
msgid "Message sent"
|
||||
msgstr ""
|
||||
|
@ -1441,15 +1445,11 @@ msgstr ""
|
|||
msgid "More than 20 albums found"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/people/faces.vue:360
|
||||
msgid "More than 20 faces found"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/labels.vue:342
|
||||
msgid "More than 20 labels found"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/people/subjects.vue:351
|
||||
#: src/pages/people/subjects.vue:395
|
||||
msgid "More than 20 people found"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1482,19 +1482,19 @@ msgstr ""
|
|||
#: src/component/photo/list.vue:237
|
||||
#: src/dialog/account/edit.vue:397
|
||||
#: src/dialog/album/edit.vue:106
|
||||
#: src/dialog/photo/files.vue:70
|
||||
#: src/dialog/photo/files.vue:67
|
||||
#: src/dialog/photo/files.vue:76
|
||||
#: src/dialog/photo/files.vue:73
|
||||
#: src/dialog/photo/files.vue:30
|
||||
#: src/dialog/photo/info.vue:31
|
||||
#: src/dialog/photo/labels.vue:48
|
||||
#: src/dialog/photo/people.vue:18
|
||||
#: src/dialog/photo/people.vue:228
|
||||
#: src/dialog/photo/people.vue:229
|
||||
#: src/pages/about/feedback.vue:144
|
||||
#: src/pages/labels.vue:337
|
||||
#: src/pages/login.vue:73
|
||||
#: src/pages/people/faces.vue:46
|
||||
#: src/pages/people/faces.vue:312
|
||||
#: src/pages/people/subjects.vue:314
|
||||
#: src/pages/people/faces.vue:48
|
||||
#: src/pages/people/faces.vue:304
|
||||
#: src/pages/people/subjects.vue:315
|
||||
#: src/share/photo/cards.vue:30
|
||||
#: src/share/photo/list.vue:34
|
||||
#: src/share/photo/list.vue:216
|
||||
|
@ -1507,8 +1507,8 @@ msgstr ""
|
|||
#: src/dialog/photo/people.vue:21
|
||||
#: src/pages/labels.vue:38
|
||||
#: src/pages/library/files.vue:40
|
||||
#: src/pages/people/faces.vue:40
|
||||
#: src/pages/people/subjects.vue:39
|
||||
#: src/pages/people/faces.vue:42
|
||||
#: src/pages/people/subjects.vue:40
|
||||
msgid "Name too long"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1532,6 +1532,7 @@ msgstr ""
|
|||
msgid "Newest first"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/people/merge.vue:15
|
||||
#: src/dialog/photo/archive.vue:15
|
||||
#: src/dialog/photo/info.vue:284
|
||||
#: src/dialog/photo/info.vue:305
|
||||
|
@ -1561,9 +1562,10 @@ msgstr ""
|
|||
|
||||
#: src/dialog/photo/people.vue:5
|
||||
#: src/pages/people/faces.vue:23
|
||||
#: src/pages/people/faces.vue:353
|
||||
#: src/pages/people/faces.vue:263
|
||||
#: src/pages/people/faces.vue:349
|
||||
#: src/pages/people/subjects.vue:26
|
||||
#: src/pages/people/subjects.vue:344
|
||||
#: src/pages/people/subjects.vue:388
|
||||
msgid "No people found"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1675,8 +1677,9 @@ msgstr ""
|
|||
msgid "One label found"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/people/faces.vue:355
|
||||
#: src/pages/people/subjects.vue:346
|
||||
#: src/pages/people/faces.vue:265
|
||||
#: src/pages/people/faces.vue:351
|
||||
#: src/pages/people/subjects.vue:390
|
||||
msgid "One person found"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1696,8 +1699,8 @@ msgstr ""
|
|||
msgid "Orange"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/files.vue:128
|
||||
#: src/dialog/photo/files.vue:125
|
||||
#: src/dialog/photo/files.vue:134
|
||||
#: src/dialog/photo/files.vue:131
|
||||
msgid "Orientation"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1709,8 +1712,8 @@ msgstr ""
|
|||
msgid "Original file names will be stored and indexed."
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/files.vue:76
|
||||
#: src/dialog/photo/files.vue:73
|
||||
#: src/dialog/photo/files.vue:82
|
||||
#: src/dialog/photo/files.vue:79
|
||||
#: src/dialog/photo/info.vue:37
|
||||
msgid "Original Name"
|
||||
msgstr ""
|
||||
|
@ -1763,7 +1766,7 @@ msgstr ""
|
|||
#: src/dialog/photo/edit.vue:267
|
||||
#: src/pages/settings/general.vue:338
|
||||
#: src/routes.js:265
|
||||
#: src/routes.js:272
|
||||
#: src/routes.js:286
|
||||
msgid "People"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1843,8 +1846,8 @@ msgstr ""
|
|||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/files.vue:108
|
||||
#: src/dialog/photo/files.vue:105
|
||||
#: src/dialog/photo/files.vue:114
|
||||
#: src/dialog/photo/files.vue:111
|
||||
msgid "Portrait"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1875,9 +1878,9 @@ msgid "Preview"
|
|||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/files.vue:34
|
||||
#: src/dialog/photo/files.vue:100
|
||||
#: src/dialog/photo/files.vue:106
|
||||
#: src/dialog/photo/files.vue:31
|
||||
#: src/dialog/photo/files.vue:97
|
||||
#: src/dialog/photo/files.vue:103
|
||||
#: src/dialog/photo/files.vue:24
|
||||
msgid "Primary"
|
||||
msgstr ""
|
||||
|
@ -1894,8 +1897,8 @@ msgstr ""
|
|||
msgid "Product Feedback"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/files.vue:116
|
||||
#: src/dialog/photo/files.vue:113
|
||||
#: src/dialog/photo/files.vue:122
|
||||
#: src/dialog/photo/files.vue:119
|
||||
msgid "Projection"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1979,8 +1982,8 @@ msgstr ""
|
|||
#: src/pages/labels.vue:91
|
||||
#: src/pages/library/errors.vue:69
|
||||
#: src/pages/library/files.vue:100
|
||||
#: src/pages/people/faces.vue:67
|
||||
#: src/pages/people/subjects.vue:97
|
||||
#: src/pages/people/faces.vue:63
|
||||
#: src/pages/people/subjects.vue:98
|
||||
msgid "Reload"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2067,7 +2070,7 @@ msgstr ""
|
|||
#: src/pages/albums.vue:109
|
||||
#: src/pages/labels.vue:81
|
||||
#: src/pages/library/errors.vue:60
|
||||
#: src/pages/people/subjects.vue:80
|
||||
#: src/pages/people/subjects.vue:81
|
||||
#: src/pages/places.vue:30
|
||||
#: src/routes.js:107
|
||||
msgid "Search"
|
||||
|
@ -2133,11 +2136,11 @@ msgstr ""
|
|||
#: src/component/navigation.vue:18
|
||||
#: src/component/navigation.vue:4
|
||||
#: src/component/navigation.vue:1305
|
||||
#: src/routes.js:301
|
||||
#: src/routes.js:313
|
||||
#: src/routes.js:325
|
||||
#: src/routes.js:337
|
||||
#: src/routes.js:349
|
||||
#: src/routes.js:315
|
||||
#: src/routes.js:327
|
||||
#: src/routes.js:339
|
||||
#: src/routes.js:351
|
||||
#: src/routes.js:363
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2198,7 +2201,7 @@ msgstr ""
|
|||
msgid "Shows more detailed log messages. Requires a restart."
|
||||
msgstr ""
|
||||
|
||||
#: src/model/file.js:186
|
||||
#: src/model/file.js:185
|
||||
msgid "Sidecar"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2215,8 +2218,8 @@ msgid "Similar"
|
|||
msgstr ""
|
||||
|
||||
#: src/dialog/account/edit.vue:220
|
||||
#: src/dialog/photo/files.vue:82
|
||||
#: src/dialog/photo/files.vue:79
|
||||
#: src/dialog/photo/files.vue:88
|
||||
#: src/dialog/photo/files.vue:85
|
||||
#: src/dialog/photo/files.vue:32
|
||||
msgid "Size"
|
||||
msgstr ""
|
||||
|
@ -2296,8 +2299,8 @@ msgstr ""
|
|||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/files.vue:64
|
||||
#: src/dialog/photo/files.vue:61
|
||||
#: src/dialog/photo/files.vue:70
|
||||
#: src/dialog/photo/files.vue:67
|
||||
msgid "Storage Folder"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2310,7 +2313,7 @@ msgid "Style"
|
|||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/details.vue:482
|
||||
#: src/model/subject.js:135
|
||||
#: src/model/subject.js:136
|
||||
msgid "Subject"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2340,7 +2343,7 @@ msgid "Teal"
|
|||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/details.vue:26
|
||||
#: src/pages/people/faces.vue:49
|
||||
#: src/pages/people/faces.vue:51
|
||||
msgid "Text too long"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2431,15 +2434,15 @@ msgid "Try again using other filters or keywords."
|
|||
msgstr ""
|
||||
|
||||
#: src/dialog/account/edit.vue:490
|
||||
#: src/dialog/photo/files.vue:88
|
||||
#: src/dialog/photo/files.vue:85
|
||||
#: src/dialog/photo/files.vue:94
|
||||
#: src/dialog/photo/files.vue:91
|
||||
#: src/dialog/photo/files.vue:33
|
||||
#: src/dialog/photo/info.vue:15
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/photo/people.vue:151
|
||||
#: src/pages/people/faces.vue:235
|
||||
#: src/pages/people/faces.vue:229
|
||||
msgid "Undo"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2449,7 +2452,7 @@ msgstr ""
|
|||
|
||||
#: src/dialog/photo/details.vue:16
|
||||
#: src/dialog/photo/info.vue:21
|
||||
#: src/model/album.js:146
|
||||
#: src/model/album.js:136
|
||||
#: src/model/photo.js:527
|
||||
#: src/model/photo.js:544
|
||||
#: src/model/photo.js:567
|
||||
|
@ -2576,7 +2579,7 @@ msgstr ""
|
|||
#: src/component/photo/cards.vue:225
|
||||
#: src/component/photo/list.vue:198
|
||||
#: src/component/photo/mosaic.vue:200
|
||||
#: src/model/file.js:184
|
||||
#: src/model/file.js:183
|
||||
#: src/model/photo.js:618
|
||||
#: src/model/photo.js:632
|
||||
#: src/options/options.js:300
|
||||
|
@ -2643,12 +2646,13 @@ msgstr ""
|
|||
msgid "Yellowstone"
|
||||
msgstr ""
|
||||
|
||||
#: src/dialog/people/merge.vue:18
|
||||
#: src/dialog/photo/archive.vue:18
|
||||
#: src/dialog/photo/files.vue:103
|
||||
#: src/dialog/photo/files.vue:111
|
||||
#: src/dialog/photo/files.vue:109
|
||||
#: src/dialog/photo/files.vue:117
|
||||
#: src/dialog/photo/files.vue:157
|
||||
#: src/dialog/photo/files.vue:100
|
||||
#: src/dialog/photo/files.vue:108
|
||||
#: src/dialog/photo/files.vue:106
|
||||
#: src/dialog/photo/files.vue:114
|
||||
#: src/dialog/photo/files.vue:154
|
||||
#: src/dialog/photo/info.vue:284
|
||||
#: src/dialog/photo/info.vue:305
|
||||
|
|
|
@ -131,26 +131,16 @@ export class Album extends RestModel {
|
|||
return DateTime.fromISO(`${date}T12:00:00Z`).toUTC();
|
||||
}
|
||||
|
||||
localDate(time) {
|
||||
if (!this.TakenAtLocal) {
|
||||
return this.utcDate();
|
||||
}
|
||||
|
||||
let zone = this.getTimeZone();
|
||||
|
||||
return DateTime.fromISO(this.localDateString(time), { zone });
|
||||
}
|
||||
|
||||
getDateString() {
|
||||
if (!this.Year || this.Year <= 1000) {
|
||||
return $gettext("Unknown");
|
||||
} else if (!this.Month || this.Month <= 0) {
|
||||
return this.localYearString();
|
||||
return this.Year.toString();
|
||||
} else if (!this.Day || this.Day <= 0) {
|
||||
return this.getDate().toLocaleString({ month: "long", year: "numeric" });
|
||||
}
|
||||
|
||||
return this.localDate().toLocaleString(DateTime.DATE_HUGE);
|
||||
return this.getDate().toLocaleString(DateTime.DATE_HUGE);
|
||||
}
|
||||
|
||||
getCreatedString() {
|
||||
|
|
|
@ -33,13 +33,11 @@ import RestModel from "model/rest";
|
|||
import { DateTime } from "luxon";
|
||||
import { config } from "../session";
|
||||
import { $gettext } from "common/vm";
|
||||
import * as src from "../common/src";
|
||||
import Api from "../common/api";
|
||||
|
||||
export class Face extends RestModel {
|
||||
constructor(values) {
|
||||
if (values && values.Marker) {
|
||||
values.Marker = new Marker(values.Marker);
|
||||
}
|
||||
|
||||
super(values);
|
||||
}
|
||||
|
||||
|
@ -48,15 +46,24 @@ export class Face extends RestModel {
|
|||
ID: "",
|
||||
Src: "",
|
||||
SubjUID: "",
|
||||
SubjSrc: "",
|
||||
FileUID: "",
|
||||
MarkerUID: "",
|
||||
Samples: 0,
|
||||
SampleRadius: 0.0,
|
||||
Collisions: 0,
|
||||
CollisionRadius: 0.0,
|
||||
Marker: new Marker(),
|
||||
Hidden: false,
|
||||
MatchedAt: "",
|
||||
CreatedAt: "",
|
||||
UpdatedAt: "",
|
||||
Name: "",
|
||||
FaceDist: 0.0,
|
||||
Size: 0,
|
||||
Score: 0,
|
||||
Review: false,
|
||||
Invalid: false,
|
||||
Thumb: "",
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -65,7 +72,7 @@ export class Face extends RestModel {
|
|||
}
|
||||
|
||||
classes(selected) {
|
||||
let classes = ["is-face", "uid-" + this.UID];
|
||||
let classes = ["is-face", "uid-" + this.ID];
|
||||
|
||||
if (this.Hidden) classes.push("is-hidden");
|
||||
if (selected) classes.push("is-selected");
|
||||
|
@ -82,11 +89,15 @@ export class Face extends RestModel {
|
|||
}
|
||||
|
||||
thumbnailUrl(size) {
|
||||
if (!this.Marker) {
|
||||
return `${config.contentUri}/svg/portrait`;
|
||||
if (!size) {
|
||||
size = "tile_160";
|
||||
}
|
||||
|
||||
return this.Marker.thumbnailUrl(size);
|
||||
if (this.Thumb) {
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken()}/${size}`;
|
||||
} else {
|
||||
return `${config.contentUri}/svg/portrait`;
|
||||
}
|
||||
}
|
||||
|
||||
getDateString() {
|
||||
|
@ -103,8 +114,35 @@ export class Face extends RestModel {
|
|||
return this.update();
|
||||
}
|
||||
|
||||
setName() {
|
||||
if (!this.Name || this.Name.trim() === "") {
|
||||
// Can't save an empty name.
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
this.SubjSrc = src.Manual;
|
||||
|
||||
const payload = { SubjSrc: this.SubjSrc, Name: this.Name };
|
||||
|
||||
return Api.put(Marker.getCollectionResource() + "/" + this.MarkerUID, payload).then((resp) => {
|
||||
if (resp && resp.data && resp.data.Name) {
|
||||
const data = resp.data;
|
||||
this.setValues({
|
||||
Name: data.Name,
|
||||
SubjSrc: data.SubjSrc,
|
||||
SubjUID: data.SubjUID,
|
||||
Review: data.Review,
|
||||
Invalid: data.Invalid,
|
||||
Thumb: data.Thumb,
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(this);
|
||||
});
|
||||
}
|
||||
|
||||
static batchSize() {
|
||||
return 120;
|
||||
return 24;
|
||||
}
|
||||
|
||||
static getCollectionResource() {
|
||||
|
|
|
@ -130,7 +130,6 @@ export class File extends RestModel {
|
|||
|
||||
download() {
|
||||
if (!this.Hash) {
|
||||
console.warn("no file hash found for download", this);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -131,7 +131,7 @@ export class Marker extends RestModel {
|
|||
}
|
||||
|
||||
static batchSize() {
|
||||
return 60;
|
||||
return 48;
|
||||
}
|
||||
|
||||
static getCollectionResource() {
|
||||
|
|
|
@ -96,6 +96,16 @@ export class Model {
|
|||
return result;
|
||||
}
|
||||
|
||||
originalValue(key) {
|
||||
if (this.__originalValues.hasOwnProperty(key) && key !== "__originalValues") {
|
||||
return this.__originalValues[key];
|
||||
} else if (this.hasOwnProperty(key) && key !== "__originalValues") {
|
||||
return this[key];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
wasChanged() {
|
||||
const changed = this.getValues(true);
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ export class Subject extends RestModel {
|
|||
Private: false,
|
||||
Excluded: false,
|
||||
FileCount: 0,
|
||||
PhotoCount: 0,
|
||||
Thumb: "",
|
||||
ThumbSrc: "",
|
||||
Metadata: {},
|
||||
|
@ -124,7 +125,7 @@ export class Subject extends RestModel {
|
|||
}
|
||||
|
||||
static batchSize() {
|
||||
return 480;
|
||||
return 60;
|
||||
}
|
||||
|
||||
static getCollectionResource() {
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
<v-layout row wrap class="search-results album-results cards-view" :class="{'select-results': selection.length > 0}">
|
||||
<v-flex
|
||||
v-for="(album, index) in results"
|
||||
:key="index"
|
||||
:key="album.UID"
|
||||
xs6 sm4 md3 xlg2 xxl1 d-flex
|
||||
>
|
||||
<v-card tile
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
<v-layout row wrap class="search-results label-results cards-view" :class="{'select-results': selection.length > 0}">
|
||||
<v-flex
|
||||
v-for="(label, index) in results"
|
||||
:key="index"
|
||||
:key="label.UID"
|
||||
xs6 sm4 md3 lg2 xxl1 d-flex
|
||||
>
|
||||
<v-card tile
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
</v-container>
|
||||
<v-list v-else-if="errors.length > 0" dense two-line class="transparent">
|
||||
<v-list-tile
|
||||
v-for="(err, index) in errors" :key="index"
|
||||
v-for="err in errors" :key="err.ID"
|
||||
avatar
|
||||
@click="showDetails(err)"
|
||||
>
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
<v-layout row wrap class="search-results file-results cards-view" :class="{'select-results': selection.length > 0}">
|
||||
<v-flex
|
||||
v-for="(model, index) in results"
|
||||
:key="index"
|
||||
:key="model.UID"
|
||||
xs6 sm4 md3 lg2 xxl1 d-flex
|
||||
>
|
||||
<v-card tile
|
||||
|
|
|
@ -8,17 +8,23 @@
|
|||
slider-color="secondary-dark"
|
||||
:height="$vuetify.breakpoint.smAndDown ? 48 : 64"
|
||||
>
|
||||
<v-tab v-for="(item, index) in tabs" :id="'tab-' + item.name" :key="index" :class="item.class" ripple
|
||||
@click="changePath(item.path)">
|
||||
<v-tab v-for="(item, index) in tabs" :id="'tab-' + item.name" :key="index" :class="item.class"
|
||||
ripple @click="changePath(item.path)">
|
||||
<v-icon v-if="$vuetify.breakpoint.smAndDown" :title="item.label">{{ item.icon }}</v-icon>
|
||||
<template v-else>
|
||||
<v-icon :size="18" :left="!rtl" :right="rtl">{{ item.icon }}</v-icon> {{ item.label }}
|
||||
<v-icon :size="18" :left="!rtl" :right="rtl">{{ item.icon }}</v-icon>
|
||||
<v-badge color="secondary-dark" :left="rtl" :right="!rtl">
|
||||
<template #badge>
|
||||
<span v-if="item.count">{{ item.count }}</span>
|
||||
</template>
|
||||
{{ item.label }}
|
||||
</v-badge>
|
||||
</template>
|
||||
</v-tab>
|
||||
|
||||
<v-tabs-items touchless>
|
||||
<v-tab-item v-for="(item, index) in tabs" :key="index" lazy>
|
||||
<component :is="item.component" :static-filter="item.filter" :active="active === index"></component>
|
||||
<component :is="item.component" :static-filter="item.filter" :active="active === index" @updateFaceCount="onUpdateFaceCount"></component>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</v-tabs>
|
||||
|
@ -59,13 +65,10 @@ export default {
|
|||
'class': '',
|
||||
'path': '/people/new',
|
||||
'icon': 'person_add',
|
||||
'count': 0,
|
||||
},
|
||||
];
|
||||
|
||||
if (config.count.people === 0) {
|
||||
tabName = "people-faces";
|
||||
}
|
||||
|
||||
let active = 0;
|
||||
|
||||
if (typeof tabName === 'string' && tabName !== '') {
|
||||
|
@ -83,7 +86,10 @@ export default {
|
|||
};
|
||||
},
|
||||
methods: {
|
||||
changePath: function (path) {
|
||||
onUpdateFaceCount(count) {
|
||||
this.tabs[1].count = count;
|
||||
},
|
||||
changePath (path) {
|
||||
if (this.$route.path !== path) {
|
||||
this.$router.replace(path);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
<template>
|
||||
<div v-infinite-scroll="loadMore" class="p-page p-page-faces" style="user-select: none"
|
||||
:infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="1200"
|
||||
:infinite-scroll-listen-for-event="'scrollRefresh'">
|
||||
<div class="p-page p-page-faces" style="user-select: none">
|
||||
|
||||
<v-form ref="form" class="p-faces-search" lazy-validation dense @submit.prevent="updateQuery">
|
||||
<v-toolbar dense flat color="secondary-light pa-0">
|
||||
|
@ -35,22 +33,21 @@
|
|||
</v-alert>
|
||||
<v-layout row wrap class="search-results face-results cards-view" :class="{'select-results': selection.length > 0}">
|
||||
<v-flex
|
||||
v-for="(model, index) in results"
|
||||
:key="index"
|
||||
v-for="model in results"
|
||||
:key="model.ID"
|
||||
xs12 sm6 md4 lg3 xl2 xxl1 d-flex
|
||||
>
|
||||
<v-card v-if="model.Marker"
|
||||
:data-id="model.Marker.UID"
|
||||
<v-card :data-id="model.ID"
|
||||
tile style="user-select: none;"
|
||||
:class="model.classes()"
|
||||
class="result accent lighten-3">
|
||||
<div class="card-background accent lighten-3"></div>
|
||||
<v-img :src="model.Marker.thumbnailUrl('tile_320')"
|
||||
<v-img :src="model.thumbnailUrl('tile_320')"
|
||||
:transition="false"
|
||||
aspect-ratio="1"
|
||||
class="accent lighten-2 clickable"
|
||||
@click.stop.prevent="$router.push(model.route(view))">
|
||||
<v-btn v-if="!model.Marker.SubjUID && !model.Hidden" :ripple="false" :depressed="false" class="input-hide"
|
||||
<v-btn v-if="!model.SubjUID && !model.Hidden" :ripple="false" :depressed="false" class="input-hide"
|
||||
icon flat small absolute :title="$gettext('Hide')"
|
||||
@click.stop.prevent="onHide(model)">
|
||||
<v-icon color="white" class="action-hide">clear</v-icon>
|
||||
|
@ -68,29 +65,27 @@
|
|||
</v-btn>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<v-layout v-else-if="model.Marker.SubjUID" row wrap align-center>
|
||||
<v-layout v-else-if="model.SubjUID" row wrap align-center>
|
||||
<v-flex xs12 class="text-xs-left pa-0">
|
||||
<v-text-field
|
||||
v-model="model.Marker.Name"
|
||||
v-model="model.Name"
|
||||
:rules="[textRule]"
|
||||
:disabled="busy"
|
||||
:readonly="false"
|
||||
browser-autocomplete="off"
|
||||
class="input-name pa-0 ma-0"
|
||||
hide-details
|
||||
single-line
|
||||
solo-inverted
|
||||
clearable
|
||||
clear-icon="eject"
|
||||
@click:clear="onClearSubject(model.Marker)"
|
||||
@change="onRename(model.Marker)"
|
||||
@keyup.enter.native="onRename(model.Marker)"
|
||||
@change="onRename(model)"
|
||||
@keyup.enter.native="onRename(model)"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<v-layout v-else row wrap align-center>
|
||||
<v-flex xs12 class="text-xs-left pa-0">
|
||||
<v-combobox
|
||||
v-model="model.Marker.Name"
|
||||
v-model="model.Name"
|
||||
style="z-index: 250"
|
||||
:items="$config.values.people"
|
||||
item-value="Name"
|
||||
|
@ -108,8 +103,8 @@
|
|||
prepend-inner-icon="person_add"
|
||||
browser-autocomplete="off"
|
||||
class="input-name pa-0 ma-0"
|
||||
@change="onRename(model.Marker)"
|
||||
@keyup.enter.native="onRename(model.Marker)"
|
||||
@change="onRename(model)"
|
||||
@keyup.enter.native="onRename(model)"
|
||||
>
|
||||
</v-combobox>
|
||||
</v-flex>
|
||||
|
@ -118,7 +113,7 @@
|
|||
</v-card>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<div class="text-xs-center my-2">
|
||||
<div class="text-xs-center mt-3 mb-2">
|
||||
<v-btn
|
||||
color="secondary" round
|
||||
:to="{name: 'all', query: { q: 'face:new' }}"
|
||||
|
@ -164,9 +159,10 @@ export default {
|
|||
scrollDisabled: true,
|
||||
loading: true,
|
||||
busy: false,
|
||||
batchSize: Face.batchSize(),
|
||||
batchSize: 999,
|
||||
offset: 0,
|
||||
page: 0,
|
||||
faceCount: 0,
|
||||
selection: [],
|
||||
settings: settings,
|
||||
filter: filter,
|
||||
|
@ -200,10 +196,6 @@ export default {
|
|||
this.filter.order = this.sortOrder();
|
||||
this.routeName = this.$route.name;
|
||||
|
||||
if (this.dirty) {
|
||||
this.lastFilter = {};
|
||||
}
|
||||
|
||||
this.search();
|
||||
}
|
||||
},
|
||||
|
@ -213,7 +205,6 @@ export default {
|
|||
this.subscriptions.push(Event.subscribe("faces", (ev, data) => this.onUpdate(ev, data)));
|
||||
|
||||
this.subscriptions.push(Event.subscribe("touchmove.top", () => this.refresh()));
|
||||
this.subscriptions.push(Event.subscribe("touchmove.bottom", () => this.loadMore()));
|
||||
},
|
||||
destroyed() {
|
||||
for (let i = 0; i < this.subscriptions.length; i++) {
|
||||
|
@ -222,20 +213,13 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
searchCount() {
|
||||
const offset = parseInt(window.localStorage.getItem("faces_offset"));
|
||||
|
||||
if(this.offset > 0 || !offset) {
|
||||
return this.batchSize;
|
||||
}
|
||||
|
||||
return offset + this.batchSize;
|
||||
return this.batchSize;
|
||||
},
|
||||
sortOrder() {
|
||||
return "relevance";
|
||||
return "samples";
|
||||
},
|
||||
setOffset(offset) {
|
||||
this.offset = offset;
|
||||
window.localStorage.setItem("faces_offset", offset);
|
||||
},
|
||||
toggleLike(ev, index) {
|
||||
const inputType = this.input.eval(ev, index);
|
||||
|
@ -375,13 +359,18 @@ export default {
|
|||
this.lastId = "";
|
||||
},
|
||||
loadMore() {
|
||||
if (this.scrollDisabled) return;
|
||||
if (this.scrollDisabled || !this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scrollDisabled = true;
|
||||
this.listen = false;
|
||||
|
||||
const count = this.dirty ? (this.page + 2) * this.batchSize : this.batchSize;
|
||||
const offset = this.dirty ? 0 : this.offset;
|
||||
// Always refresh all faces for now.
|
||||
this.dirty = true;
|
||||
|
||||
const count = this.batchSize;
|
||||
const offset = 0;
|
||||
|
||||
const params = {
|
||||
count: count,
|
||||
|
@ -397,22 +386,14 @@ export default {
|
|||
Face.search(params).then(resp => {
|
||||
this.results = this.dirty ? resp.models : this.results.concat(resp.models);
|
||||
|
||||
this.scrollDisabled = (resp.count < resp.limit);
|
||||
this.setFaceCount(this.results.length);
|
||||
|
||||
if (this.scrollDisabled) {
|
||||
this.setOffset(resp.offset);
|
||||
if (this.results.length > 1) {
|
||||
this.$notify.info(this.$gettextInterpolate(this.$gettext("All %{n} people loaded"), {n: this.results.length}));
|
||||
}
|
||||
if (!this.results.length) {
|
||||
this.$notify.warn(this.$gettext("No people found"));
|
||||
} else if (this.results.length === 1) {
|
||||
this.$notify.info(this.$gettext("One person found"));
|
||||
} else {
|
||||
this.setOffset(resp.offset + resp.limit);
|
||||
this.page++;
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.$root.$el.clientHeight <= window.document.documentElement.clientHeight + 300) {
|
||||
this.$emit("scrollRefresh");
|
||||
}
|
||||
});
|
||||
this.$notify.info(this.$gettextInterpolate(this.$gettext("%{n} people found"), {n: this.results.length}));
|
||||
}
|
||||
}).catch(() => {
|
||||
this.scrollDisabled = false;
|
||||
|
@ -458,11 +439,15 @@ export default {
|
|||
return params;
|
||||
},
|
||||
refresh() {
|
||||
if (this.loading) return;
|
||||
if (this.loading || !this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.page = 0;
|
||||
this.dirty = true;
|
||||
this.scrollDisabled = false;
|
||||
|
||||
this.loadMore();
|
||||
},
|
||||
search() {
|
||||
|
@ -470,7 +455,7 @@ export default {
|
|||
|
||||
// Don't query the same data more than once
|
||||
if (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) {
|
||||
this.$nextTick(() => this.$emit("scrollRefresh"));
|
||||
this.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -487,24 +472,14 @@ export default {
|
|||
this.offset = resp.limit;
|
||||
this.results = resp.models;
|
||||
|
||||
this.scrollDisabled = (resp.count < resp.limit);
|
||||
this.setFaceCount(this.results.length);
|
||||
|
||||
if (this.scrollDisabled) {
|
||||
if (!this.results.length) {
|
||||
this.$notify.warn(this.$gettext("No people found"));
|
||||
} else if (this.results.length === 1) {
|
||||
this.$notify.info(this.$gettext("One person found"));
|
||||
} else {
|
||||
this.$notify.info(this.$gettextInterpolate(this.$gettext("%{n} people found"), {n: this.results.length}));
|
||||
}
|
||||
if (!this.results.length) {
|
||||
this.$notify.warn(this.$gettext("No people found"));
|
||||
} else if (this.results.length === 1) {
|
||||
this.$notify.info(this.$gettext("One person found"));
|
||||
} else {
|
||||
this.$notify.info(this.$gettext('More than 20 faces found'));
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.$root.$el.clientHeight <= window.document.documentElement.clientHeight + 300) {
|
||||
this.$emit("scrollRefresh");
|
||||
}
|
||||
});
|
||||
this.$notify.info(this.$gettextInterpolate(this.$gettext("%{n} people found"), {n: this.results.length}));
|
||||
}
|
||||
}).finally(() => {
|
||||
this.dirty = false;
|
||||
|
@ -514,19 +489,35 @@ export default {
|
|||
},
|
||||
onShow(face) {
|
||||
this.busy = true;
|
||||
face.show().finally(() => this.busy = false);
|
||||
face.show().finally(() => {
|
||||
this.busy = false;
|
||||
this.changeFaceCount(1);
|
||||
});
|
||||
},
|
||||
onHide(face) {
|
||||
this.busy = true;
|
||||
face.hide().finally(() => this.busy = false);
|
||||
face.hide().finally(() => {
|
||||
this.busy = false;
|
||||
this.changeFaceCount(-1);
|
||||
});
|
||||
},
|
||||
onClearSubject(marker) {
|
||||
onRename(model) {
|
||||
this.busy = true;
|
||||
marker.clearSubject(marker).finally(() => this.busy = false);
|
||||
this.$notify.blockUI();
|
||||
|
||||
model.setName().finally(() => {
|
||||
this.$notify.unblockUI();
|
||||
this.busy = false;
|
||||
this.changeFaceCount(-1);
|
||||
});
|
||||
},
|
||||
onRename(marker) {
|
||||
this.busy = true;
|
||||
marker.rename().finally(() => this.busy = false);
|
||||
changeFaceCount(count) {
|
||||
this.faceCount = this.faceCount + count;
|
||||
this.$emit('updateFaceCount', this.faceCount);
|
||||
},
|
||||
setFaceCount(count) {
|
||||
this.faceCount = count;
|
||||
this.$emit('updateFaceCount', this.faceCount);
|
||||
},
|
||||
onUpdate(ev, data) {
|
||||
if (!this.listen) return;
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
<v-layout row wrap class="search-results subject-results cards-view" :class="{'select-results': selection.length > 0}">
|
||||
<v-flex
|
||||
v-for="(model, index) in results"
|
||||
:key="index"
|
||||
:key="model.UID"
|
||||
xs6 sm4 md3 lg2 xxl1 d-flex
|
||||
>
|
||||
<v-card tile
|
||||
|
@ -132,11 +132,11 @@
|
|||
</div>
|
||||
|
||||
<div class="caption mb-2">
|
||||
<button v-if="model.FileCount === 1">
|
||||
<button v-if="model.PhotoCount === 1">
|
||||
<translate>Contains one entry.</translate>
|
||||
</button>
|
||||
<button v-else-if="model.FileCount > 0">
|
||||
<translate :translate-params="{n: model.FileCount}">Contains %{n} entries.</translate>
|
||||
<button v-else-if="model.PhotoCount > 0">
|
||||
<translate :translate-params="{n: model.PhotoCount}">Contains %{n} entries.</translate>
|
||||
</button>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
@ -145,6 +145,8 @@
|
|||
</v-layout>
|
||||
</v-container>
|
||||
</v-container>
|
||||
<p-people-merge-dialog lazy :show="merge.show" :subj1="merge.subj1" :subj2="merge.subj2" @cancel="onCancelMerge"
|
||||
@confirm="onMerge"></p-people-merge-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -191,6 +193,11 @@ export default {
|
|||
titleRule: v => v.length <= this.$config.get("clip") || this.$gettext("Name too long"),
|
||||
input: new Input(),
|
||||
lastId: "",
|
||||
merge: {
|
||||
subj1: null,
|
||||
subj2: null,
|
||||
show: false,
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
@ -208,10 +215,6 @@ export default {
|
|||
this.filter.order = this.sortOrder();
|
||||
this.routeName = this.$route.name;
|
||||
|
||||
if (this.dirty) {
|
||||
this.lastFilter = {};
|
||||
}
|
||||
|
||||
this.search();
|
||||
}
|
||||
},
|
||||
|
@ -229,6 +232,39 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
onSave(m) {
|
||||
const existing = this.$config.getPerson(m.Name);
|
||||
|
||||
if(!existing) {
|
||||
m.update();
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing.UID === m.UID) {
|
||||
// Name didn't change.
|
||||
return;
|
||||
}
|
||||
|
||||
this.merge.subj1 = m;
|
||||
this.merge.subj2 = existing;
|
||||
this.merge.show = true;
|
||||
},
|
||||
onCancelMerge() {
|
||||
this.merge.subj1.Name = this.merge.subj1.originalValue("Name");
|
||||
this.merge.show = false;
|
||||
this.merge.subj1 = null;
|
||||
this.merge.subj2 = null;
|
||||
},
|
||||
onMerge() {
|
||||
this.merge.show = false;
|
||||
this.$notify.blockUI();
|
||||
this.merge.subj1.update().finally(() => {
|
||||
this.merge.subj1 = null;
|
||||
this.merge.subj2 = null;
|
||||
this.$notify.unblockUI();
|
||||
this.refresh();
|
||||
});
|
||||
},
|
||||
searchCount() {
|
||||
const offset = parseInt(window.localStorage.getItem("subjects_offset"));
|
||||
|
||||
|
@ -326,9 +362,6 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
onSave(m) {
|
||||
m.update();
|
||||
},
|
||||
showAll() {
|
||||
this.filter.all = "true";
|
||||
this.updateQuery();
|
||||
|
@ -383,7 +416,9 @@ export default {
|
|||
this.lastId = "";
|
||||
},
|
||||
loadMore() {
|
||||
if (this.scrollDisabled) return;
|
||||
if (this.scrollDisabled || !this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scrollDisabled = true;
|
||||
this.listen = false;
|
||||
|
@ -466,11 +501,15 @@ export default {
|
|||
return params;
|
||||
},
|
||||
refresh() {
|
||||
if (this.loading) return;
|
||||
if (this.loading || !this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.page = 0;
|
||||
this.dirty = true;
|
||||
this.scrollDisabled = false;
|
||||
|
||||
this.loadMore();
|
||||
},
|
||||
search() {
|
||||
|
|
|
@ -45,7 +45,7 @@ import Feedback from "pages/about/feedback.vue";
|
|||
import License from "pages/about/license.vue";
|
||||
import Help from "pages/help.vue";
|
||||
import { $gettext } from "common/vm";
|
||||
import { session } from "./session";
|
||||
import { config, session } from "./session";
|
||||
|
||||
const c = window.__CONFIG__;
|
||||
const appName = c.name;
|
||||
|
@ -264,6 +264,20 @@ export default [
|
|||
component: People,
|
||||
meta: { title: $gettext("People"), auth: true, background: "application-light" },
|
||||
props: { tab: "people-subjects" },
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (!config || !from || !from.name || from.name.startsWith("people")) {
|
||||
next();
|
||||
} else {
|
||||
config.load().finally(() => {
|
||||
// Open new faces tab when there are no people.
|
||||
if (config.values.count.people === 0) {
|
||||
next({ name: "people_faces" });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "people_faces",
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<v-layout row wrap class="search-results album-results cards-view" :class="{'select-results': selection.length > 0}">
|
||||
<v-flex
|
||||
v-for="(album, index) in results"
|
||||
:key="index"
|
||||
:key="album.UID"
|
||||
xs6 sm4 md3 xlg2 xxl1 d-flex
|
||||
>
|
||||
<v-card tile
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<v-layout row wrap class="search-results photo-results cards-view" :class="{'select-results': selectMode}">
|
||||
<v-flex
|
||||
v-for="(photo, index) in photos"
|
||||
:key="index"
|
||||
:key="photo.ID"
|
||||
xs12 sm6 md4 lg3 xlg2 xxxl1 d-flex
|
||||
>
|
||||
<v-card tile
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<v-layout row wrap class="search-results photo-results mosaic-view" :class="{'select-results': selectMode}">
|
||||
<v-flex
|
||||
v-for="(photo, index) in photos"
|
||||
:key="index"
|
||||
:key="photo.ID"
|
||||
xs4 sm3 md2 lg1 d-flex
|
||||
>
|
||||
<v-card tile
|
||||
|
|
|
@ -194,3 +194,27 @@ test.meta("testID", "labels-004")("Delete label", async (t) => {
|
|||
.typeText(Selector(".input-label input"), "Dome")
|
||||
.click(Selector("button.p-photo-label-add"));
|
||||
});
|
||||
|
||||
/*Does not work on sqlite
|
||||
test.skip("testID", "labels-005")("Check label count", async (t) => {
|
||||
await page.openNav();
|
||||
await t.click(Selector(".nav-labels"));
|
||||
await page.search("cat");
|
||||
const LabelCat = await Selector("a.is-label", { timeout: 55000 }).nth(0).getAttribute("data-uid");
|
||||
const CatCaption = await Selector("a[data-uid=" + LabelCat + "] div.caption").innerText;
|
||||
console.log(CatCaption);
|
||||
await t.click(Selector("a.is-label").withAttribute("data-uid", LabelCat));
|
||||
const countPhotosCat = await Selector("div.is-photo").count;
|
||||
await t.expect(CatCaption).contains(countPhotosCat.toString());
|
||||
console.log(countPhotosCat);
|
||||
await page.openNav();
|
||||
await t.click(Selector(".nav-labels"));
|
||||
await page.search("people");
|
||||
const LabelPeople = await Selector("a.is-label", { timeout: 55000 }).nth(0).getAttribute("data-uid");
|
||||
const PeopleCaption = await Selector("a[data-uid=" + LabelCat + "] div.caption").innerText;
|
||||
console.log(PeopleCaption);
|
||||
await t.click(Selector("a.is-label").withAttribute("data-uid", LabelPeople));
|
||||
const countPhotosPeople = await Selector("div.is-photo").count;
|
||||
await t.expect(CatCaption).contains(countPhotosPeople.toString());
|
||||
console.log(countPhotosPeople);
|
||||
});*/
|
||||
|
|
|
@ -80,8 +80,20 @@ test.meta("testID", "people-002")("Add + Rename", async (t) => {
|
|||
.click(Selector("#tab-people"))
|
||||
.expect(Selector("div.input-name input").nth(0).value)
|
||||
.contains("Jane Doe")
|
||||
.typeText(Selector("div.input-name input").nth(0), "Max Mu", { replace: true })
|
||||
.click("button.action-close");
|
||||
await page.openNav();
|
||||
await t
|
||||
.click(Selector(".nav-people"))
|
||||
.click(Selector("a[data-uid=" + JaneUID + "] div.v-card__title"))
|
||||
.typeText(Selector("div.input-rename input"), "Max Mu", { replace: true })
|
||||
.pressKey("enter")
|
||||
.expect(Selector("a[data-uid=" + JaneUID + "] div.v-card__title").innerText)
|
||||
.contains("Max Mu")
|
||||
.click(Selector("a.is-subject").withAttribute("data-uid", JaneUID));
|
||||
await t.eval(() => location.reload());
|
||||
await page.editSelected();
|
||||
await t
|
||||
.click(Selector("#tab-people"))
|
||||
.expect(Selector("div.input-name input").nth(0).value)
|
||||
.contains("Max Mu")
|
||||
.click("button.action-next")
|
||||
|
@ -99,35 +111,6 @@ test.meta("testID", "people-002")("Add + Rename", async (t) => {
|
|||
.pressKey("enter");
|
||||
const countPhotosSubjectAfterRename = await Selector("div.is-photo").count;
|
||||
await t.expect(countPhotosSubjectAfterRename).eql(countPhotosSubject);
|
||||
await page.openNav();
|
||||
await t
|
||||
.click(Selector(".nav-people"))
|
||||
.expect(Selector("a[data-uid=" + JaneUID + "] div.v-card__title").innerText)
|
||||
.contains("Max Mu")
|
||||
.click(Selector("a[data-uid=" + JaneUID + "] div.v-card__title"))
|
||||
.typeText(Selector("div.input-rename input"), "Jane Mu", { replace: true })
|
||||
.pressKey("enter")
|
||||
.expect(Selector("a[data-uid=" + JaneUID + "] div.v-card__title").innerText)
|
||||
.contains("Jane Mu")
|
||||
.click(Selector("a.is-subject").withAttribute("data-uid", JaneUID))
|
||||
.expect(Selector("div.input-search input").value)
|
||||
.contains("person:jane-mu");
|
||||
await page.toggleSelectNthPhoto(0);
|
||||
await page.toggleSelectNthPhoto(1);
|
||||
await page.toggleSelectNthPhoto(2);
|
||||
await page.editSelected();
|
||||
await t
|
||||
.click(Selector("#tab-people"))
|
||||
.expect(Selector("div.input-name input").nth(0).value)
|
||||
.contains("Jane Mu")
|
||||
.click("button.action-next")
|
||||
.expect(Selector("div.input-name input").nth(0).value)
|
||||
.contains("Jane Mu")
|
||||
.click("button.action-next")
|
||||
.expect(Selector("div.input-name input").nth(0).value)
|
||||
.contains("Jane Mu")
|
||||
.click("button.action-close");
|
||||
await page.clearSelection();
|
||||
});
|
||||
|
||||
test.meta("testID", "people-003")("Add + Reject + Star", async (t) => {
|
||||
|
@ -148,25 +131,9 @@ test.meta("testID", "people-003")("Add + Reject + Star", async (t) => {
|
|||
.click(Selector("#tab-people-subjects > a"));
|
||||
await t.typeText(Selector("div.input-search input"), "Andrea").pressKey("enter");
|
||||
const AndreaUID = await Selector("a.is-subject").nth(0).getAttribute("data-uid");
|
||||
await page.openNav();
|
||||
await t.click(Selector(".nav-browse"));
|
||||
await page.search("face:new filmpreis");
|
||||
await page.toggleSelectNthPhoto(0);
|
||||
await page.editSelected();
|
||||
await t
|
||||
.click(Selector("#tab-people"))
|
||||
.expect(Selector("div.input-name input").nth(0).value)
|
||||
.eql("")
|
||||
.typeText(Selector("div.input-name input").nth(0), "Andrea Doe", { replace: true })
|
||||
.click(Selector("div").withText("Andrea Doe"))
|
||||
.expect(Selector("div.input-name input").nth(0).value)
|
||||
.contains("Andrea Doe")
|
||||
.click("button.action-close");
|
||||
await page.clearSelection();
|
||||
await page.openNav();
|
||||
await t
|
||||
.click(Selector(".nav-people"))
|
||||
.click(Selector("a.is-subject").withAttribute("data-uid", AndreaUID));
|
||||
await t.click(Selector("a.is-subject").withAttribute("data-uid", AndreaUID));
|
||||
await t.eval(() => location.reload());
|
||||
await t.wait(6000);
|
||||
const countPhotosAndreaAfterAdd = await Selector("div.is-photo").count;
|
||||
await page.toggleSelectNthPhoto(1);
|
||||
await page.editSelected();
|
||||
|
@ -186,10 +153,12 @@ test.meta("testID", "people-003")("Add + Reject + Star", async (t) => {
|
|||
const countPhotosAndreaAfterReject = await Selector("div.is-photo").count;
|
||||
const Diff = countPhotosAndreaAfterAdd - countPhotosAndreaAfterReject;
|
||||
await t
|
||||
.typeText(Selector("div.input-search input"), "Nicole", { replace: true })
|
||||
.typeText(Selector("div.input-search input"), "person:nicole", { replace: true })
|
||||
.pressKey("enter");
|
||||
await t.eval(() => location.reload());
|
||||
await t.wait(6000);
|
||||
const countPhotosNicole = await Selector("div.is-photo").count;
|
||||
await t.expect(countPhotosNicole).eql(Diff);
|
||||
await t.expect(Diff).eql(countPhotosNicole);
|
||||
await page.openNav();
|
||||
await t
|
||||
.click(Selector(".nav-people"))
|
||||
|
|
|
@ -49,6 +49,15 @@ describe("common/config", () => {
|
|||
assert.equal(config.values.settings.ui.language, "en");
|
||||
});
|
||||
|
||||
it("should test constructor with empty values", () => {
|
||||
const storage = new StorageShim();
|
||||
const values = {};
|
||||
const config = new Config(storage, values);
|
||||
assert.equal(config.debug, true);
|
||||
assert.equal(config.demo, false);
|
||||
assert.equal(config.apiUri, "/api/v1");
|
||||
});
|
||||
|
||||
it("should store values", () => {
|
||||
const storage = new StorageShim();
|
||||
const values = { siteTitle: "Foo", country: "Germany", city: "Hamburg" };
|
||||
|
@ -79,6 +88,93 @@ describe("common/config", () => {
|
|||
assert.equal(config2.feature("places"), true);
|
||||
assert.equal(config2.feature("download"), true);
|
||||
});
|
||||
|
||||
it("should test get name", () => {
|
||||
const result = config2.getPerson("a");
|
||||
assert.equal(result, null);
|
||||
|
||||
const result2 = config2.getPerson("Andrea Sander");
|
||||
assert.equal(result2.UID, "jr0jgyx2viicdnf7");
|
||||
|
||||
const result3 = config2.getPerson("Otto Sander");
|
||||
assert.equal(result3.UID, "jr0jgyx2viicdn88");
|
||||
});
|
||||
|
||||
it("should create, update and delete people", () => {
|
||||
const storage = new StorageShim();
|
||||
const values = { Debug: true, siteTitle: "Foo", country: "Germany", city: "Hamburg" };
|
||||
|
||||
const config = new Config(storage, values);
|
||||
config.onPeople(".created");
|
||||
assert.empty(config.values.people);
|
||||
config.onPeople(".created", {
|
||||
entities: [
|
||||
{
|
||||
UID: "abc123",
|
||||
Name: "Test Name",
|
||||
Keywords: ["Test", "Name"],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.equal(config.values.people[0].Name, "Test Name");
|
||||
config.onPeople(".updated", {
|
||||
entities: [
|
||||
{
|
||||
UID: "abc123",
|
||||
Name: "New Name",
|
||||
Keywords: ["New", "Name"],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.equal(config.values.people[0].Name, "New Name");
|
||||
config.onPeople(".deleted", {
|
||||
entities: [
|
||||
{
|
||||
UID: "abc123",
|
||||
Name: "New Name",
|
||||
Keywords: ["New", "Name"],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.equal(config.values.people[0].Name, "New Name");
|
||||
});
|
||||
|
||||
it("should return if language is rtl", () => {
|
||||
const result = config2.rtl();
|
||||
assert.equal(result, false);
|
||||
const storage = new StorageShim();
|
||||
const values = {
|
||||
Debug: true,
|
||||
siteTitle: "Foo",
|
||||
country: "Germany",
|
||||
city: "Hamburg",
|
||||
settings: {
|
||||
ui: {
|
||||
language: "he",
|
||||
},
|
||||
},
|
||||
};
|
||||
const config = new Config(storage, values);
|
||||
const result2 = config.rtl();
|
||||
assert.equal(result2, true);
|
||||
const values2 = { siteTitle: "Foo", country: "Germany", city: "Hamburg" };
|
||||
const config3 = new Config(storage, values2);
|
||||
const result3 = config3.rtl();
|
||||
assert.equal(result3, false);
|
||||
});
|
||||
|
||||
it("should return album categories", () => {
|
||||
const myConfig = new Config(new StorageShim(), Object.assign({}, window.__CONFIG__));
|
||||
const result = myConfig.albumCategories();
|
||||
assert.equal(result[0], "Animal");
|
||||
const newValues = {
|
||||
albumCategories: ["Mouse"],
|
||||
};
|
||||
myConfig.setValues(newValues);
|
||||
const result2 = myConfig.albumCategories();
|
||||
assert.equal(result2[0], "Mouse");
|
||||
});
|
||||
|
||||
//TODO
|
||||
/*it.only("should test onCount", () => {
|
||||
const items = [{}, {}, {}];
|
||||
|
|
|
@ -23,7 +23,7 @@ const clientConfig = {
|
|||
test: true,
|
||||
demo: false,
|
||||
sponsor: true,
|
||||
albumCategories: null,
|
||||
albumCategories: ["Animal", "Holiday"],
|
||||
albums: [
|
||||
{
|
||||
ID: 69,
|
||||
|
@ -218,6 +218,23 @@ const clientConfig = {
|
|||
Name: "Unknown",
|
||||
},
|
||||
],
|
||||
people: [
|
||||
{
|
||||
UID: "jr0jgyx2viicdnf7",
|
||||
Name: "Andrea Sander",
|
||||
Keywords: ["andrea"],
|
||||
},
|
||||
{
|
||||
UID: "jr0jgyx2viicdn88",
|
||||
Name: "Otto Sander",
|
||||
Keywords: ["andrea"],
|
||||
},
|
||||
{
|
||||
UID: "jr0jgzi2qmp5wt97",
|
||||
Name: "Otto Sander",
|
||||
Keywords: ["otto", "sander"],
|
||||
},
|
||||
],
|
||||
thumbs: [
|
||||
{
|
||||
size: "fit_720",
|
||||
|
|
|
@ -293,6 +293,47 @@ Mock.onDelete("api/v1/photos/undefined/like").reply(200, { status: "ok" }, mockH
|
|||
Mock.onPost("api/v1/albums/undefined/like").reply(200, { status: "ok" }, mockHeaders);
|
||||
Mock.onDelete("api/v1/albums/undefined/like").reply(200, { status: "ok" }, mockHeaders);
|
||||
Mock.onGet("api/v1/config").reply(200, clientConfig, mockHeaders);
|
||||
Mock.onPut("api/v1/markers/mBC123ghytr", { Review: false, Invalid: false }).reply(
|
||||
200,
|
||||
{
|
||||
success: "ok",
|
||||
},
|
||||
mockHeaders
|
||||
);
|
||||
Mock.onPut("api/v1/markers/mCC123ghytr", { Review: false, Invalid: true }).reply(
|
||||
200,
|
||||
{
|
||||
success: "ok",
|
||||
},
|
||||
mockHeaders
|
||||
);
|
||||
Mock.onPut("api/v1/markers/mDC123ghytr", { SubjSrc: "manual", Name: "testname" }).reply(
|
||||
200,
|
||||
{
|
||||
success: "ok",
|
||||
},
|
||||
mockHeaders
|
||||
);
|
||||
Mock.onDelete("api/v1/markers/mEC123ghytr/subject").reply(200, { success: "ok" }, mockHeaders);
|
||||
Mock.onPut("api/v1/faces/f123ghytrfggd", { Hidden: false }).reply(
|
||||
200,
|
||||
{
|
||||
success: "ok",
|
||||
},
|
||||
mockHeaders
|
||||
);
|
||||
Mock.onPut("api/v1/faces/f123ghytrfggd", { Hidden: true }).reply(
|
||||
200,
|
||||
{
|
||||
success: "ok",
|
||||
},
|
||||
mockHeaders
|
||||
);
|
||||
Mock.onPost("api/v1/subjects/s123ghytrfggd/like").reply(200, { status: "ok" }, mockHeaders);
|
||||
Mock.onDelete("api/v1/subjects/s123ghytrfggd/like").reply(200, { status: "ok" }, mockHeaders);
|
||||
Mock.onGet("api/v1/config/options").reply(200, { success: "ok" }, mockHeaders);
|
||||
Mock.onPost("api/v1/config/options").reply(200, { success: "ok" }, mockHeaders);
|
||||
|
||||
//Mock.onPost().reply(200);
|
||||
//Mock.onDelete().reply(200);
|
||||
/*
|
||||
|
|
|
@ -64,6 +64,25 @@ describe("model/album", () => {
|
|||
const album = new Album(values);
|
||||
const result = album.thumbnailUrl("xyz");
|
||||
assert.equal(result, "/api/v1/t/d6b24d688564f7ddc7b245a414f003a8d8ff5a67/public/xyz");
|
||||
|
||||
const values2 = {
|
||||
ID: 5,
|
||||
Title: "Christmas 2019",
|
||||
Slug: "christmas-2019",
|
||||
UID: 66,
|
||||
};
|
||||
const album2 = new Album(values2);
|
||||
const result2 = album2.thumbnailUrl("xyz");
|
||||
assert.equal(result2, "/api/v1/albums/66/t/public/xyz");
|
||||
|
||||
const values3 = {
|
||||
ID: 5,
|
||||
Title: "Christmas 2019",
|
||||
Slug: "christmas-2019",
|
||||
};
|
||||
const album3 = new Album(values3);
|
||||
const result3 = album3.thumbnailUrl("xyz");
|
||||
assert.equal(result3, "/api/v1/svg/album");
|
||||
});
|
||||
|
||||
it("should get created date string", () => {
|
||||
|
@ -93,6 +112,21 @@ describe("model/album", () => {
|
|||
assert.equal(result, "May 2019");
|
||||
});
|
||||
|
||||
it("should get album date string with invalid month", () => {
|
||||
const values = {
|
||||
ID: 5,
|
||||
Title: "Christmas 2019",
|
||||
Slug: "christmas-2019",
|
||||
CreatedAt: "2012-07-08T14:45:39Z",
|
||||
Day: 1,
|
||||
Month: -5,
|
||||
Year: 2000,
|
||||
};
|
||||
const album = new Album(values);
|
||||
const result = album.getDateString();
|
||||
assert.equal(result, "2000");
|
||||
});
|
||||
|
||||
it("should get album date string with invalid year", () => {
|
||||
const values = {
|
||||
ID: 5,
|
||||
|
@ -108,6 +142,21 @@ describe("model/album", () => {
|
|||
assert.equal(result, "Unknown");
|
||||
});
|
||||
|
||||
it("should get album date string", () => {
|
||||
const values = {
|
||||
ID: 5,
|
||||
Title: "Christmas 2019",
|
||||
Slug: "christmas-2019",
|
||||
CreatedAt: "2012-07-08T14:45:39Z",
|
||||
Day: 1,
|
||||
Month: 5,
|
||||
Year: 2000,
|
||||
};
|
||||
const album = new Album(values);
|
||||
const result = album.getDateString();
|
||||
assert.equal(result, "Monday, May 1, 2000");
|
||||
});
|
||||
|
||||
it("should get day string", () => {
|
||||
const values = {
|
||||
ID: 5,
|
||||
|
|
52
frontend/tests/unit/model/config-options_test.js
Normal file
52
frontend/tests/unit/model/config-options_test.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
import "../fixtures";
|
||||
import ConfigOptions from "model/config-options";
|
||||
|
||||
let chai = require("chai/chai");
|
||||
let assert = chai.assert;
|
||||
|
||||
describe("model/config-options", () => {
|
||||
it("should get options defaults", () => {
|
||||
const values = {};
|
||||
const options = new ConfigOptions(values);
|
||||
const result = options.getDefaults();
|
||||
assert.equal(result.Debug, false);
|
||||
assert.equal(result.ReadOnly, false);
|
||||
assert.equal(result.ThumbSize, 0);
|
||||
});
|
||||
|
||||
it("should test changed", () => {
|
||||
const values = {};
|
||||
const options = new ConfigOptions(values);
|
||||
assert.equal(options.changed(), false);
|
||||
});
|
||||
|
||||
it("should load options", (done) => {
|
||||
const values = {};
|
||||
const options = new ConfigOptions(values);
|
||||
options
|
||||
.load()
|
||||
.then((response) => {
|
||||
assert.equal(response.success, "ok");
|
||||
done();
|
||||
})
|
||||
.catch((error) => {
|
||||
done(error);
|
||||
});
|
||||
assert.equal(options.changed(), false);
|
||||
});
|
||||
|
||||
it("should save options", (done) => {
|
||||
const values = { Debug: true };
|
||||
const options = new ConfigOptions(values);
|
||||
options
|
||||
.save()
|
||||
.then((response) => {
|
||||
assert.equal(response.success, "ok");
|
||||
done();
|
||||
})
|
||||
.catch((error) => {
|
||||
done(error);
|
||||
});
|
||||
assert.equal(options.changed(), false);
|
||||
});
|
||||
});
|
121
frontend/tests/unit/model/face_test.js
Normal file
121
frontend/tests/unit/model/face_test.js
Normal file
|
@ -0,0 +1,121 @@
|
|||
import "../fixtures";
|
||||
import Face from "model/face";
|
||||
|
||||
let chai = require("chai/chai");
|
||||
let assert = chai.assert;
|
||||
|
||||
describe("model/face", () => {
|
||||
it("should get face defaults", () => {
|
||||
const values = {};
|
||||
const face = new Face(values);
|
||||
const result = face.getDefaults();
|
||||
assert.equal(result.ID, "");
|
||||
assert.equal(result.SampleRadius, 0.0);
|
||||
});
|
||||
|
||||
it("should get route view", () => {
|
||||
const values = { ID: "f123ghytrfggd", Samples: 5 };
|
||||
const face = new Face(values);
|
||||
const result = face.route("test");
|
||||
assert.equal(result.name, "test");
|
||||
assert.equal(result.query.q, "face:f123ghytrfggd");
|
||||
});
|
||||
|
||||
it("should return classes", () => {
|
||||
const values = { ID: "f123ghytrfggd", Samples: 5 };
|
||||
const face = new Face(values);
|
||||
const result = face.classes(true);
|
||||
assert.include(result, "is-face");
|
||||
assert.include(result, "uid-f123ghytrfggd");
|
||||
assert.include(result, "is-selected");
|
||||
assert.notInclude(result, "is-hidden");
|
||||
const result2 = face.classes(false);
|
||||
assert.include(result2, "is-face");
|
||||
assert.include(result2, "uid-f123ghytrfggd");
|
||||
assert.notInclude(result2, "is-selected");
|
||||
assert.notInclude(result2, "is-hidden");
|
||||
const values2 = { ID: "f123ghytrfggd", Samples: 5, Hidden: true };
|
||||
const face2 = new Face(values2);
|
||||
const result3 = face2.classes(true);
|
||||
assert.include(result3, "is-face");
|
||||
assert.include(result3, "uid-f123ghytrfggd");
|
||||
assert.include(result3, "is-selected");
|
||||
assert.include(result3, "is-hidden");
|
||||
});
|
||||
|
||||
it("should get face entity name", () => {
|
||||
const values = { ID: "f123ghytrfggd", Samples: 5 };
|
||||
const face = new Face(values);
|
||||
const result = face.getEntityName();
|
||||
assert.equal(result, "f123ghytrfggd");
|
||||
});
|
||||
|
||||
it("should get face title", () => {
|
||||
const values = { ID: "f123ghytrfggd", Samples: 5 };
|
||||
const face = new Face(values);
|
||||
const result = face.getTitle();
|
||||
assert.equal(result, undefined);
|
||||
});
|
||||
|
||||
it("should get thumbnail url", () => {
|
||||
const values = {
|
||||
ID: "f123ghytrfggd",
|
||||
Samples: 5,
|
||||
MarkerUID: "ABC123ghytr",
|
||||
FileUID: "fhjouohnnmnd",
|
||||
Name: "",
|
||||
Thumb: "7ca759a2b788cc5bcc08dbbce9854ff94a2f94d1",
|
||||
};
|
||||
|
||||
const face = new Face(values);
|
||||
const result = face.thumbnailUrl("xyz");
|
||||
|
||||
assert.equal(result, "/api/v1/t/7ca759a2b788cc5bcc08dbbce9854ff94a2f94d1/public/xyz");
|
||||
|
||||
const values2 = { ID: "f123ghytrfggd", Samples: 5, Thumb: "7ca759a2b788cc5bcc08dbbce9854ff94a2f94d1" };
|
||||
const face2 = new Face(values2);
|
||||
const result2 = face2.thumbnailUrl();
|
||||
|
||||
assert.equal(result2, "/api/v1/t/7ca759a2b788cc5bcc08dbbce9854ff94a2f94d1/public/tile_160");
|
||||
});
|
||||
|
||||
it("should get date string", () => {
|
||||
const values = {
|
||||
ID: "f123ghytrfggd",
|
||||
Samples: 5,
|
||||
CreatedAt: "2012-07-08T14:45:39Z",
|
||||
};
|
||||
const face = new Face(values);
|
||||
const result = face.getDateString();
|
||||
assert.equal(result, "Jul 8, 2012, 2:45 PM");
|
||||
});
|
||||
|
||||
it("show and hide face", () => {
|
||||
const values = {
|
||||
ID: "f123ghytrfggd",
|
||||
Samples: 5,
|
||||
CreatedAt: "2012-07-08T14:45:39Z",
|
||||
Hidden: true,
|
||||
};
|
||||
const face = new Face(values);
|
||||
assert.equal(face.Hidden, true);
|
||||
face.show();
|
||||
assert.equal(face.Hidden, false);
|
||||
face.hide();
|
||||
assert.equal(face.Hidden, true);
|
||||
});
|
||||
|
||||
it("should return batch size", () => {
|
||||
assert.equal(Face.batchSize(), 24);
|
||||
});
|
||||
|
||||
it("should get collection resource", () => {
|
||||
const result = Face.getCollectionResource();
|
||||
assert.equal(result, "faces");
|
||||
});
|
||||
|
||||
it("should get model name", () => {
|
||||
const result = Face.getModelName();
|
||||
assert.equal(result, "Face");
|
||||
});
|
||||
});
|
|
@ -95,6 +95,16 @@ describe("model/file", () => {
|
|||
};
|
||||
const file3 = new File(values3);
|
||||
assert.equal(file3.thumbnailUrl("tile_224"), "/api/v1/svg/raw");
|
||||
const values4 = {
|
||||
InstanceID: 5,
|
||||
UID: "ABC123",
|
||||
Hash: "54ghtfd",
|
||||
Type: "jpg",
|
||||
Name: "1/2/IMG123.jpg",
|
||||
Sidecar: true,
|
||||
};
|
||||
const file4 = new File(values4);
|
||||
assert.equal(file4.thumbnailUrl("tile_224"), "/api/v1/svg/file");
|
||||
});
|
||||
|
||||
it("should return download url", () => {
|
||||
|
@ -109,6 +119,17 @@ describe("model/file", () => {
|
|||
assert.equal(file.getDownloadUrl("abc"), "/api/v1/dl/54ghtfd?t=2lbh9x09");
|
||||
});
|
||||
|
||||
it("should not download as hash is missing", () => {
|
||||
const values = {
|
||||
InstanceID: 5,
|
||||
UID: "ABC123",
|
||||
Type: "jpg",
|
||||
Name: "1/2/IMG123.jpg",
|
||||
};
|
||||
const file = new File(values);
|
||||
assert.equal(file.download(), undefined);
|
||||
});
|
||||
|
||||
it("should calculate size", () => {
|
||||
const values = {
|
||||
InstanceID: 5,
|
||||
|
|
|
@ -59,6 +59,25 @@ describe("model/label", () => {
|
|||
const label = new Label(values);
|
||||
const result = label.thumbnailUrl("xyz");
|
||||
assert.equal(result, "/api/v1/t/c6b24d688564f7ddc7b245a414f003a8d8ff5a67/public/xyz");
|
||||
|
||||
const values2 = {
|
||||
ID: 5,
|
||||
UID: "ABC123",
|
||||
Name: "Black Cat",
|
||||
Slug: "black-cat",
|
||||
};
|
||||
const label2 = new Label(values2);
|
||||
const result2 = label2.thumbnailUrl("xyz");
|
||||
assert.equal(result2, "/api/v1/labels/ABC123/t/public/xyz");
|
||||
|
||||
const values3 = {
|
||||
ID: 5,
|
||||
Name: "Black Cat",
|
||||
Slug: "black-cat",
|
||||
};
|
||||
const label3 = new Label(values3);
|
||||
const result3 = label3.thumbnailUrl("xyz");
|
||||
assert.equal(result3, "/api/v1/svg/label");
|
||||
});
|
||||
|
||||
it("should get date string", () => {
|
||||
|
|
218
frontend/tests/unit/model/marker_test.js
Normal file
218
frontend/tests/unit/model/marker_test.js
Normal file
|
@ -0,0 +1,218 @@
|
|||
import "../fixtures";
|
||||
import Marker from "model/marker";
|
||||
|
||||
let chai = require("chai/chai");
|
||||
let assert = chai.assert;
|
||||
|
||||
describe("model/marker", () => {
|
||||
it("should get marker defaults", () => {
|
||||
const values = { FileUID: "fghjojp" };
|
||||
const marker = new Marker(values);
|
||||
const result = marker.getDefaults();
|
||||
assert.equal(result.UID, "");
|
||||
assert.equal(result.FileUID, "");
|
||||
});
|
||||
|
||||
it("should get route view", () => {
|
||||
const values = { UID: "ABC123ghytr", FileUID: "fhjouohnnmnd", Type: "face", Src: "image" };
|
||||
const marker = new Marker(values);
|
||||
const result = marker.route("test");
|
||||
assert.equal(result.name, "test");
|
||||
assert.equal(result.query.q, "marker:ABC123ghytr");
|
||||
});
|
||||
|
||||
it("should return classes", () => {
|
||||
const values = { UID: "ABC123ghytr", FileUID: "fhjouohnnmnd", Type: "face", Src: "image" };
|
||||
const marker = new Marker(values);
|
||||
const result = marker.classes(true);
|
||||
assert.include(result, "is-marker");
|
||||
assert.include(result, "uid-ABC123ghytr");
|
||||
assert.include(result, "is-selected");
|
||||
assert.notInclude(result, "is-review");
|
||||
assert.notInclude(result, "is-invalid");
|
||||
const result2 = marker.classes(false);
|
||||
assert.include(result2, "is-marker");
|
||||
assert.include(result2, "uid-ABC123ghytr");
|
||||
assert.notInclude(result2, "is-selected");
|
||||
assert.notInclude(result2, "is-review");
|
||||
assert.notInclude(result2, "is-invalid");
|
||||
const values2 = {
|
||||
UID: "mBC123ghytr",
|
||||
FileUID: "fhjouohnnmnd",
|
||||
Type: "face",
|
||||
Src: "image",
|
||||
Invalid: true,
|
||||
Review: true,
|
||||
};
|
||||
const marker2 = new Marker(values2);
|
||||
const result3 = marker2.classes(true);
|
||||
assert.include(result3, "is-marker");
|
||||
assert.include(result3, "uid-mBC123ghytr");
|
||||
assert.include(result3, "is-selected");
|
||||
assert.include(result3, "is-review");
|
||||
assert.include(result3, "is-invalid");
|
||||
});
|
||||
|
||||
it("should get marker entity name", () => {
|
||||
const values = {
|
||||
UID: "ABC123ghytr",
|
||||
FileUID: "fhjouohnnmnd",
|
||||
Type: "face",
|
||||
Src: "image",
|
||||
Name: "test",
|
||||
};
|
||||
const marker = new Marker(values);
|
||||
const result = marker.getEntityName();
|
||||
assert.equal(result, "test");
|
||||
});
|
||||
|
||||
it("should get marker title", () => {
|
||||
const values = {
|
||||
UID: "ABC123ghytr",
|
||||
FileUID: "fhjouohnnmnd",
|
||||
Type: "face",
|
||||
Src: "image",
|
||||
Name: "test",
|
||||
};
|
||||
const marker = new Marker(values);
|
||||
const result = marker.getTitle();
|
||||
assert.equal(result, "test");
|
||||
});
|
||||
|
||||
it("should get thumbnail url", () => {
|
||||
const values = { UID: "ABC123ghytr", FileUID: "fhjouohnnmnd", Type: "face", Src: "image" };
|
||||
const marker = new Marker(values);
|
||||
const result = marker.thumbnailUrl("xyz");
|
||||
assert.equal(result, "/api/v1/svg/portrait");
|
||||
|
||||
const values2 = {
|
||||
UID: "ABC123ghytr",
|
||||
FileUID: "fhjouohnnmnd",
|
||||
Type: "face",
|
||||
Src: "image",
|
||||
Thumb: "nicethumbuid",
|
||||
};
|
||||
const marker2 = new Marker(values2);
|
||||
const result2 = marker2.thumbnailUrl();
|
||||
assert.equal(result2, "/api/v1/t/nicethumbuid/public/tile_160");
|
||||
});
|
||||
|
||||
it("should get date string", () => {
|
||||
const values = {
|
||||
UID: "ABC123ghytr",
|
||||
FileUID: "fhjouohnnmnd",
|
||||
Type: "face",
|
||||
Src: "image",
|
||||
CreatedAt: "2012-07-08T14:45:39Z",
|
||||
};
|
||||
const marker = new Marker(values);
|
||||
const result = marker.getDateString();
|
||||
assert.equal(result, "Jul 8, 2012, 2:45 PM");
|
||||
});
|
||||
|
||||
it("should approve marker", () => {
|
||||
const values = {
|
||||
UID: "mBC123ghytr",
|
||||
FileUID: "fhjouohnnmnd",
|
||||
Type: "face",
|
||||
Src: "image",
|
||||
Invalid: true,
|
||||
Review: true,
|
||||
};
|
||||
const marker = new Marker(values);
|
||||
assert.equal(marker.Review, true);
|
||||
assert.equal(marker.Invalid, true);
|
||||
marker.approve();
|
||||
assert.equal(marker.Review, false);
|
||||
assert.equal(marker.Invalid, false);
|
||||
});
|
||||
|
||||
it("should reject marker", () => {
|
||||
const values = {
|
||||
UID: "mCC123ghytr",
|
||||
FileUID: "fhjouohnnmnd",
|
||||
Type: "face",
|
||||
Src: "image",
|
||||
Invalid: false,
|
||||
Review: true,
|
||||
};
|
||||
const marker = new Marker(values);
|
||||
assert.equal(marker.Review, true);
|
||||
assert.equal(marker.Invalid, false);
|
||||
marker.reject();
|
||||
assert.equal(marker.Review, false);
|
||||
assert.equal(marker.Invalid, true);
|
||||
});
|
||||
|
||||
it("should rename marker", (done) => {
|
||||
const values = {
|
||||
UID: "mDC123ghytr",
|
||||
FileUID: "fhjouohnnmnd",
|
||||
Type: "face",
|
||||
Src: "image",
|
||||
Subject: "skhljkpigh",
|
||||
Name: "",
|
||||
SubjSrc: "manual",
|
||||
};
|
||||
const marker = new Marker(values);
|
||||
assert.equal(marker.Name, "");
|
||||
marker.rename();
|
||||
assert.equal(marker.Name, "");
|
||||
const values2 = {
|
||||
UID: "mDC123ghytr",
|
||||
FileUID: "fhjouohnnmnd",
|
||||
Type: "face",
|
||||
Src: "image",
|
||||
Subject: "skhljkpigh",
|
||||
Name: "testname",
|
||||
SubjSrc: "manual",
|
||||
};
|
||||
const marker2 = new Marker(values2);
|
||||
assert.equal(marker2.Name, "testname");
|
||||
marker2
|
||||
.rename()
|
||||
.then((response) => {
|
||||
assert.equal(response.success, "ok");
|
||||
done();
|
||||
})
|
||||
.catch((error) => {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
|
||||
it("should clear subject", (done) => {
|
||||
const values = {
|
||||
UID: "mEC123ghytr",
|
||||
FileUID: "fhjouohnnmnd",
|
||||
Type: "face",
|
||||
Src: "image",
|
||||
Subject: "skhljkpigh",
|
||||
Name: "testname",
|
||||
SubjSrc: "manual",
|
||||
};
|
||||
const marker = new Marker(values);
|
||||
marker
|
||||
.clearSubject()
|
||||
.then((response) => {
|
||||
assert.equal(response.success, "ok");
|
||||
done();
|
||||
})
|
||||
.catch((error) => {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return batch size", () => {
|
||||
assert.equal(Marker.batchSize(), 48);
|
||||
});
|
||||
|
||||
it("should get collection resource", () => {
|
||||
const result = Marker.getCollectionResource();
|
||||
assert.equal(result, "markers");
|
||||
});
|
||||
|
||||
it("should get model name", () => {
|
||||
const result = Marker.getModelName();
|
||||
assert.equal(result, "Marker");
|
||||
});
|
||||
});
|
216
frontend/tests/unit/model/subject_test.js
Normal file
216
frontend/tests/unit/model/subject_test.js
Normal file
|
@ -0,0 +1,216 @@
|
|||
import "../fixtures";
|
||||
import Subject from "model/subject";
|
||||
|
||||
let chai = require("chai/chai");
|
||||
let assert = chai.assert;
|
||||
|
||||
describe("model/subject", () => {
|
||||
it("should get face defaults", () => {
|
||||
const values = {};
|
||||
const subject = new Subject(values);
|
||||
const result = subject.getDefaults();
|
||||
assert.equal(result.UID, "");
|
||||
assert.equal(result.Favorite, false);
|
||||
});
|
||||
|
||||
it("should get route view", () => {
|
||||
const values = { UID: "s123ghytrfggd", Type: "person", Src: "manual" };
|
||||
const subject = new Subject(values);
|
||||
const result = subject.route("test");
|
||||
assert.equal(result.name, "test");
|
||||
assert.equal(result.query.q, "subject:s123ghytrfggd");
|
||||
const values2 = {
|
||||
UID: "s123ghytrfggd",
|
||||
Type: "person",
|
||||
Src: "manual",
|
||||
Name: "Jane Doe",
|
||||
Slug: "jane-doe",
|
||||
};
|
||||
const subject2 = new Subject(values2);
|
||||
const result2 = subject2.route("test");
|
||||
assert.equal(result2.name, "test");
|
||||
assert.equal(result2.query.q, "person:jane-doe");
|
||||
});
|
||||
|
||||
it("should return classes", () => {
|
||||
const values = {
|
||||
UID: "s123ghytrfggd",
|
||||
Type: "person",
|
||||
Src: "manual",
|
||||
Name: "Jane Doe",
|
||||
Slug: "jane-doe",
|
||||
Favorite: false,
|
||||
Excluded: true,
|
||||
Private: true,
|
||||
};
|
||||
const subject = new Subject(values);
|
||||
const result = subject.classes(true);
|
||||
assert.include(result, "is-subject");
|
||||
assert.include(result, "uid-s123ghytrfggd");
|
||||
assert.include(result, "is-selected");
|
||||
assert.notInclude(result, "is-favorite");
|
||||
assert.include(result, "is-private");
|
||||
assert.include(result, "is-excluded");
|
||||
const values2 = {
|
||||
UID: "s123ghytrfggd",
|
||||
Type: "person",
|
||||
Src: "manual",
|
||||
Name: "Jane Doe",
|
||||
Slug: "jane-doe",
|
||||
Favorite: true,
|
||||
Excluded: false,
|
||||
Private: false,
|
||||
};
|
||||
const subject2 = new Subject(values2);
|
||||
const result2 = subject2.classes(false);
|
||||
assert.include(result2, "is-subject");
|
||||
assert.include(result2, "uid-s123ghytrfggd");
|
||||
assert.notInclude(result2, "is-selected");
|
||||
assert.include(result2, "is-favorite");
|
||||
assert.notInclude(result2, "is-private");
|
||||
assert.notInclude(result2, "is-excluded");
|
||||
});
|
||||
|
||||
it("should get subject entity name", () => {
|
||||
const values = {
|
||||
UID: "s123ghytrfggd",
|
||||
Type: "person",
|
||||
Src: "manual",
|
||||
Name: "Jane Doe",
|
||||
Slug: "jane-doe",
|
||||
Favorite: false,
|
||||
Excluded: true,
|
||||
Private: true,
|
||||
};
|
||||
const subject = new Subject(values);
|
||||
const result = subject.getEntityName();
|
||||
assert.equal(result, "jane-doe");
|
||||
});
|
||||
|
||||
it("should get subject title", () => {
|
||||
const values = {
|
||||
UID: "s123ghytrfggd",
|
||||
Type: "person",
|
||||
Src: "manual",
|
||||
Name: "Jane Doe",
|
||||
Slug: "jane-doe",
|
||||
Favorite: false,
|
||||
Excluded: true,
|
||||
Private: true,
|
||||
};
|
||||
const subject = new Subject(values);
|
||||
const result = subject.getTitle();
|
||||
assert.equal(result, "Jane Doe");
|
||||
});
|
||||
|
||||
it("should get thumbnail url", () => {
|
||||
const values = {
|
||||
UID: "s123ghytrfggd",
|
||||
Type: "person",
|
||||
Src: "manual",
|
||||
Name: "Jane Doe",
|
||||
Slug: "jane-doe",
|
||||
Favorite: false,
|
||||
Excluded: true,
|
||||
Private: true,
|
||||
Thumb: "nicethumb",
|
||||
};
|
||||
const subject = new Subject(values);
|
||||
const result = subject.thumbnailUrl("xyz");
|
||||
assert.equal(result, "/api/v1/t/nicethumb/public/xyz");
|
||||
const result2 = subject.thumbnailUrl();
|
||||
assert.equal(result2, "/api/v1/t/nicethumb/public/tile_160");
|
||||
const values2 = {
|
||||
UID: "s123ghytrfggd",
|
||||
Type: "person",
|
||||
Src: "manual",
|
||||
Name: "Jane Doe",
|
||||
Slug: "jane-doe",
|
||||
Favorite: false,
|
||||
Excluded: true,
|
||||
Private: true,
|
||||
};
|
||||
const subject2 = new Subject(values2);
|
||||
const result3 = subject2.thumbnailUrl("xyz");
|
||||
assert.equal(result3, "/api/v1/svg/portrait");
|
||||
});
|
||||
|
||||
it("should get date string", () => {
|
||||
const values = {
|
||||
UID: "s123ghytrfggd",
|
||||
Type: "person",
|
||||
Src: "manual",
|
||||
Name: "Jane Doe",
|
||||
Slug: "jane-doe",
|
||||
Favorite: false,
|
||||
Excluded: true,
|
||||
Private: true,
|
||||
Thumb: "nicethumb",
|
||||
CreatedAt: "2012-07-08T14:45:39Z",
|
||||
};
|
||||
const subject = new Subject(values);
|
||||
const result = subject.getDateString();
|
||||
assert.equal(result, "Jul 8, 2012, 2:45 PM");
|
||||
});
|
||||
|
||||
it("should like subject", () => {
|
||||
const values = {
|
||||
UID: "s123ghytrfggd",
|
||||
Type: "person",
|
||||
Src: "manual",
|
||||
Name: "Jane Doe",
|
||||
Slug: "jane-doe",
|
||||
Favorite: false,
|
||||
};
|
||||
const subject = new Subject(values);
|
||||
assert.equal(subject.Favorite, false);
|
||||
subject.like();
|
||||
assert.equal(subject.Favorite, true);
|
||||
});
|
||||
|
||||
it("should unlike subject", () => {
|
||||
const values = {
|
||||
UID: "s123ghytrfggd",
|
||||
Type: "person",
|
||||
Src: "manual",
|
||||
Name: "Jane Doe",
|
||||
Slug: "jane-doe",
|
||||
Favorite: true,
|
||||
};
|
||||
const subject = new Subject(values);
|
||||
assert.equal(subject.Favorite, true);
|
||||
subject.unlike();
|
||||
assert.equal(subject.Favorite, false);
|
||||
});
|
||||
|
||||
it("should toggle like", () => {
|
||||
const values = {
|
||||
UID: "s123ghytrfggd",
|
||||
Type: "person",
|
||||
Src: "manual",
|
||||
Name: "Jane Doe",
|
||||
Slug: "jane-doe",
|
||||
Favorite: true,
|
||||
};
|
||||
const subject = new Subject(values);
|
||||
assert.equal(subject.Favorite, true);
|
||||
subject.toggleLike();
|
||||
assert.equal(subject.Favorite, false);
|
||||
subject.toggleLike();
|
||||
assert.equal(subject.Favorite, true);
|
||||
});
|
||||
|
||||
it("should return batch size", () => {
|
||||
assert.equal(Subject.batchSize(), 60);
|
||||
});
|
||||
|
||||
it("should get collection resource", () => {
|
||||
const result = Subject.getCollectionResource();
|
||||
assert.equal(result, "subjects");
|
||||
});
|
||||
|
||||
it("should get model name", () => {
|
||||
const result = Subject.getModelName();
|
||||
assert.equal(result, "Subject");
|
||||
});
|
||||
});
|
|
@ -24,6 +24,28 @@ describe("model/thumb", () => {
|
|||
assert.equal(result.uid, "");
|
||||
});
|
||||
|
||||
it("should get id", () => {
|
||||
const values = {
|
||||
uid: "55",
|
||||
};
|
||||
const thumb = new Thumb(values);
|
||||
assert.equal(thumb.getId(), "55");
|
||||
});
|
||||
|
||||
it("should return hasId", () => {
|
||||
const values = {
|
||||
uid: "55",
|
||||
};
|
||||
const thumb = new Thumb(values);
|
||||
assert.equal(thumb.hasId(), true);
|
||||
|
||||
const values2 = {
|
||||
title: "",
|
||||
};
|
||||
const thumb2 = new Thumb(values2);
|
||||
assert.equal(thumb2.hasId(), false);
|
||||
});
|
||||
|
||||
it("should toggle like", () => {
|
||||
const values = {
|
||||
uid: "55",
|
||||
|
@ -125,6 +147,30 @@ describe("model/thumb", () => {
|
|||
assert.equal(result2[0].description, "Nice description 2");
|
||||
assert.equal(result2[0].original_w, 500);
|
||||
assert.equal(result2.length, 1);
|
||||
const values5 = {
|
||||
ID: 8,
|
||||
UID: "ABC123",
|
||||
Description: "Nice description 2",
|
||||
Hash: "abc345",
|
||||
Files: [
|
||||
{
|
||||
UID: "123fgb",
|
||||
Name: "1980/01/superCuteKitten.jpg",
|
||||
Primary: true,
|
||||
Type: "mov",
|
||||
Width: 500,
|
||||
Height: 600,
|
||||
Hash: "1xxbgdt53",
|
||||
},
|
||||
],
|
||||
};
|
||||
const photo4 = new Photo(values5);
|
||||
const Photos3 = [photo3, photo2, photo4];
|
||||
const result3 = Thumb.fromFiles(Photos3);
|
||||
assert.equal(result3.length, 1);
|
||||
assert.equal(result3[0].uid, "ABC123");
|
||||
assert.equal(result3[0].description, "Nice description 2");
|
||||
assert.equal(result3[0].original_w, 500);
|
||||
});
|
||||
|
||||
it("should test from files", () => {
|
||||
|
|
34
go.mod
34
go.mod
|
@ -55,13 +55,12 @@ require (
|
|||
github.com/studio-b12/gowebdav v0.0.0-20210917133250-a3a86976a1df
|
||||
github.com/tensorflow/tensorflow v1.15.2
|
||||
github.com/tidwall/gjson v1.9.1
|
||||
github.com/ugorji/go v1.2.6 // indirect
|
||||
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6
|
||||
github.com/urfave/cli v1.22.5
|
||||
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
||||
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6
|
||||
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 // indirect
|
||||
gonum.org/v1/gonum v0.9.3
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
|
@ -69,4 +68,33 @@ require (
|
|||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
go 1.16
|
||||
require (
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.5.0 // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/schema v1.2.0 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gosimple/unidecode v1.0.0 // indirect
|
||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/tidwall/match v1.0.3 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.6 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/appengine v1.6.6 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
|
||||
go 1.17
|
||||
|
|
4
go.sum
4
go.sum
|
@ -464,8 +464,8 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
|
|||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf h1:R150MpwJIv1MpS0N/pc+NhTM8ajzvlmxlY5OYsrevXQ=
|
||||
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6 h1:Z04ewVs7JhXaYkmDhBERPi41gnltfQpMWDnTnQbaCqk=
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
|
|
@ -526,10 +526,10 @@ func DownloadAlbum(router *gin.RouterGroup) {
|
|||
}
|
||||
log.Infof("download: added %s as %s", txt.Quote(file.FileName), txt.Quote(alias))
|
||||
} else {
|
||||
log.Errorf("download: file %s is missing", txt.Quote(file.FileName))
|
||||
log.Errorf("download: failed finding %s", txt.Quote(file.FileName))
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("download: album zip %s created in %s", txt.Quote(zipFileName), time.Since(start))
|
||||
log.Infof("download: created %s [%s]", txt.Quote(zipFileName), time.Since(start))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -49,6 +49,12 @@ func logError(prefix string, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
func logWarn(prefix string, err error) {
|
||||
if err != nil {
|
||||
log.Warnf("%s: %s", prefix, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateClientConfig() {
|
||||
conf := service.Config()
|
||||
|
||||
|
|
|
@ -66,10 +66,10 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Update precalculated photo and file counts.
|
||||
logError("photos", entity.UpdatePhotoCounts())
|
||||
logWarn("index", entity.UpdateCounts())
|
||||
|
||||
// Update album, subject, and label preview thumbs.
|
||||
logError("photos", query.UpdatePreviews())
|
||||
// Update album, subject, and label cover thumbs.
|
||||
logWarn("index", query.UpdateCovers())
|
||||
|
||||
UpdateClientConfig()
|
||||
|
||||
|
@ -128,10 +128,10 @@ func BatchPhotosRestore(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Update precalculated photo and file counts.
|
||||
logError("photos", entity.UpdatePhotoCounts())
|
||||
logWarn("index", entity.UpdateCounts())
|
||||
|
||||
// Update album, subject, and label preview thumbs.
|
||||
logError("photos", query.UpdatePreviews())
|
||||
// Update album, subject, and label cover thumbs.
|
||||
logWarn("index", query.UpdateCovers())
|
||||
|
||||
UpdateClientConfig()
|
||||
|
||||
|
@ -193,7 +193,7 @@ func BatchPhotosApprove(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// BatchAlbumsDelete permanently deletes multiple albums.
|
||||
// BatchAlbumsDelete permanently removes multiple albums.
|
||||
//
|
||||
// POST /api/v1/batch/albums/delete
|
||||
func BatchAlbumsDelete(router *gin.RouterGroup) {
|
||||
|
@ -264,7 +264,7 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Update precalculated photo and file counts.
|
||||
logError("photos", entity.UpdatePhotoCounts())
|
||||
logWarn("index", entity.UpdateCounts())
|
||||
|
||||
if photos, err := query.PhotoSelection(f); err == nil {
|
||||
for _, p := range photos {
|
||||
|
@ -328,7 +328,7 @@ func BatchLabelsDelete(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// BatchPhotosDelete permanently deletes multiple photos from the archive.
|
||||
// BatchPhotosDelete permanently removes multiple photos from the archive.
|
||||
//
|
||||
// POST /api/v1/batch/photos/delete
|
||||
func BatchPhotosDelete(router *gin.RouterGroup) {
|
||||
|
@ -382,7 +382,7 @@ func BatchPhotosDelete(router *gin.RouterGroup) {
|
|||
// Any photos deleted?
|
||||
if len(deleted) > 0 {
|
||||
// Update precalculated photo and file counts.
|
||||
logError("photos", entity.UpdatePhotoCounts())
|
||||
logWarn("index", entity.UpdateCounts())
|
||||
|
||||
UpdateClientConfig()
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ func RemoveFromFolderCache(rootName string) {
|
|||
|
||||
cache.Delete(cacheKey)
|
||||
|
||||
if err := query.UpdateAlbumFolderPreviews(); err != nil {
|
||||
if err := query.UpdateAlbumFolderCovers(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ func RemoveFromAlbumCoverCache(uid string) {
|
|||
log.Debugf("removed %s from cache", cacheKey)
|
||||
}
|
||||
|
||||
if err := query.UpdateAlbumPreviews(); err != nil {
|
||||
if err := query.UpdateAlbumCovers(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ func RemoveFromAlbumCoverCache(uid string) {
|
|||
func FlushCoverCache() {
|
||||
service.CoverCache().Flush()
|
||||
|
||||
if err := query.UpdatePreviews(); err != nil {
|
||||
if err := query.UpdateCovers(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -81,7 +80,7 @@ func SaveConfigOptions(router *gin.RouterGroup) {
|
|||
v := make(valueMap)
|
||||
|
||||
if fs.FileExists(fileName) {
|
||||
yamlData, err := ioutil.ReadFile(fileName)
|
||||
yamlData, err := os.ReadFile(fileName)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("options: %s", err)
|
||||
|
@ -118,7 +117,7 @@ func SaveConfigOptions(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Write YAML data to file.
|
||||
if err := ioutil.WriteFile(fileName, yamlData, os.ModePerm); err != nil {
|
||||
if err := os.WriteFile(fileName, yamlData, os.ModePerm); err != nil {
|
||||
log.Errorf("options: %s", err)
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
|
@ -76,7 +76,7 @@ func AlbumCover(router *gin.RouterGroup) {
|
|||
f, err := query.AlbumCoverByUID(uid)
|
||||
|
||||
if err != nil {
|
||||
log.Debugf("%s: no photos yet, using generic image for %s", albumCover, uid)
|
||||
log.Debugf("%s: %s contains no photos, using generic cover", albumCover, uid)
|
||||
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
|
||||
return
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ func AlbumCover(router *gin.RouterGroup) {
|
|||
fileName := photoprism.FileName(f.FileRoot, f.FileName)
|
||||
|
||||
if !fs.FileExists(fileName) {
|
||||
log.Errorf("%s: could not find original for %s", albumCover, fileName)
|
||||
log.Errorf("%s: found no original for %s", albumCover, fileName)
|
||||
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
|
||||
|
||||
// Set missing flag so that the file doesn't show up in search results anymore.
|
||||
|
|
|
@ -15,7 +15,7 @@ func TestAlbumCover(t *testing.T) {
|
|||
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("album has no photo (because is not existing)", func(t *testing.T) {
|
||||
t.Run("album contains no photos (because is not existing)", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
AlbumCover(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/albums/987-986435/t/"+conf.PreviewToken()+"/tile_500")
|
||||
|
|
|
@ -85,7 +85,7 @@ func FolderCover(router *gin.RouterGroup) {
|
|||
f, err := query.FolderCoverByUID(uid)
|
||||
|
||||
if err != nil {
|
||||
log.Debugf("%s: no photos yet, using generic image for %s", folderCover, uid)
|
||||
log.Debugf("%s: %s contains no photos, using generic cover", folderCover, uid)
|
||||
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ func StartImport(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
if len(f.Albums) > 0 {
|
||||
log.Debugf("import: files will be added to album %s", strings.Join(f.Albums, " and "))
|
||||
log.Debugf("import: adding files to album %s", strings.Join(f.Albums, " and "))
|
||||
opt.Albums = f.Albums
|
||||
}
|
||||
|
||||
|
@ -111,9 +111,9 @@ func StartImport(router *gin.RouterGroup) {
|
|||
|
||||
UpdateClientConfig()
|
||||
|
||||
// Update album, label, and subject preview images.
|
||||
if err := query.UpdatePreviews(); err != nil {
|
||||
log.Errorf("import: %s (update previews)", err)
|
||||
// Update album, label, and subject cover thumbs.
|
||||
if err := query.UpdateCovers(); err != nil {
|
||||
log.Warnf("index: %s (update covers)", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, i18n.Response{Code: http.StatusOK, Msg: msg})
|
||||
|
|
|
@ -4,10 +4,11 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
|
@ -26,7 +27,7 @@ func findFileMarker(c *gin.Context) (file *entity.File, marker *entity.Marker, e
|
|||
|
||||
// Check feature flags.
|
||||
conf := service.Config()
|
||||
if !conf.Settings().Features.People || !conf.Settings().Features.Edit {
|
||||
if !conf.Settings().Features.People {
|
||||
AbortFeatureDisabled(c)
|
||||
return nil, nil, fmt.Errorf("feature disabled")
|
||||
}
|
||||
|
@ -70,27 +71,27 @@ func UpdateMarker(router *gin.RouterGroup) {
|
|||
file, marker, err := findFileMarker(c)
|
||||
|
||||
if err != nil {
|
||||
log.Debugf("api: %s (update marker)", err)
|
||||
log.Debugf("marker: %s (find)", err)
|
||||
return
|
||||
}
|
||||
|
||||
markerForm, err := form.NewMarker(*marker)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("photo: %s (new marker form)", err)
|
||||
log.Errorf("marker: %s (new form)", err)
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.BindJSON(&markerForm); err != nil {
|
||||
log.Errorf("photo: %s (update marker form)", err)
|
||||
log.Errorf("marker: %s (update form)", err)
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Save marker.
|
||||
if changed, err := marker.SaveForm(markerForm); err != nil {
|
||||
log.Errorf("photo: %s (save marker form)", err)
|
||||
log.Errorf("marker: %s", err)
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
} else if changed {
|
||||
|
@ -98,15 +99,15 @@ func UpdateMarker(router *gin.RouterGroup) {
|
|||
if res, err := service.Faces().Optimize(); err != nil {
|
||||
log.Errorf("faces: %s (optimize)", err)
|
||||
} else if res.Merged > 0 {
|
||||
log.Infof("faces: %d clusters merged", res.Merged)
|
||||
log.Infof("faces: merged %s", english.Plural(res.Merged, "cluster", "clusters"))
|
||||
}
|
||||
}
|
||||
|
||||
if err := query.UpdateSubjectPreviews(); err != nil {
|
||||
log.Errorf("faces: %s (update previews)", err)
|
||||
if err := query.UpdateSubjectCovers(); err != nil {
|
||||
log.Errorf("faces: %s (update covers)", err)
|
||||
}
|
||||
|
||||
if err := entity.UpdateSubjectFileCounts(); err != nil {
|
||||
if err := entity.UpdateSubjectCounts(); err != nil {
|
||||
log.Errorf("faces: %s (update counts)", err)
|
||||
}
|
||||
}
|
||||
|
@ -148,9 +149,9 @@ func ClearMarkerSubject(router *gin.RouterGroup) {
|
|||
log.Errorf("faces: %s (clear subject)", err)
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
} else if err := query.UpdateSubjectPreviews(); err != nil {
|
||||
log.Errorf("faces: %s (update previews)", err)
|
||||
} else if err := entity.UpdateSubjectFileCounts(); err != nil {
|
||||
} else if err := query.UpdateSubjectCovers(); err != nil {
|
||||
log.Errorf("faces: %s (update covers)", err)
|
||||
} else if err := entity.UpdateSubjectCounts(); err != nil {
|
||||
log.Errorf("faces: %s (update counts)", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -81,11 +81,11 @@ func PhotoUnstack(router *gin.RouterGroup) {
|
|||
AbortEntityNotFound(c)
|
||||
return
|
||||
} else if related.Len() == 0 {
|
||||
log.Errorf("photo: no files found for %s (unstack)", txt.Quote(baseName))
|
||||
log.Errorf("photo: found no files for %s (unstack)", txt.Quote(baseName))
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
} else if related.Main == nil {
|
||||
log.Errorf("photo: no main file found for %s (unstack)", txt.Quote(baseName))
|
||||
log.Errorf("photo: found no main file for %s (unstack)", txt.Quote(baseName))
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
|
|||
// Handle error...
|
||||
log.Errorf("photo: %s (unstack %s)", err.Error(), txt.Quote(r.BaseName()))
|
||||
|
||||
// Remove new photo from database.
|
||||
// Remove new photo from index.
|
||||
if _, err := newPhoto.Delete(true); err != nil {
|
||||
log.Errorf("photo: %s (unstack %s)", err.Error(), txt.Quote(r.BaseName()))
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ func SharePreview(router *gin.RouterGroup) {
|
|||
|
||||
var f form.PhotoSearch
|
||||
|
||||
// Previews may only contain public content in shared albums.
|
||||
// Covers may only contain public content in shared albums.
|
||||
f.Album = share
|
||||
f.Public = true
|
||||
f.Private = false
|
||||
|
|
|
@ -106,7 +106,8 @@ func UpdateSubject(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
if _, err := m.UpdateName(f.SubjName); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
log.Errorf("subject: %s", err)
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@ func CreateZip(router *gin.RouterGroup) {
|
|||
|
||||
elapsed := int(time.Since(start).Seconds())
|
||||
|
||||
log.Infof("download: zip %s created in %s", txt.Quote(zipBaseName), time.Since(start))
|
||||
log.Infof("download: created %s [%s]", txt.Quote(zipBaseName), time.Since(start))
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "message": i18n.Msg(i18n.MsgZipCreatedIn, elapsed), "filename": zipBaseName})
|
||||
})
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -35,10 +34,10 @@ func main() {
|
|||
fileName := "rules.yml"
|
||||
|
||||
if !fs.FileExists(fileName) {
|
||||
log.Panicf("classify: label rules not found in %s", txt.Quote(filepath.Base(fileName)))
|
||||
log.Panicf("classify: found no label rules in %s", txt.Quote(filepath.Base(fileName)))
|
||||
}
|
||||
|
||||
yamlConfig, err := ioutil.ReadFile(fileName)
|
||||
yamlConfig, err := os.ReadFile(fileName)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
|
@ -49,7 +48,7 @@ func (t *TensorFlow) File(filename string) (result Labels, err error) {
|
|||
return result, nil
|
||||
}
|
||||
|
||||
imageBuffer, err := ioutil.ReadFile(filename)
|
||||
imageBuffer, err := os.ReadFile(filename)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package classify
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
|
@ -84,7 +84,7 @@ func TestTensorFlow_Labels(t *testing.T) {
|
|||
t.Run("chameleon_lime.jpg", func(t *testing.T) {
|
||||
tensorFlow := NewTest(t)
|
||||
|
||||
if imageBuffer, err := ioutil.ReadFile(examplesPath + "/chameleon_lime.jpg"); err != nil {
|
||||
if imageBuffer, err := os.ReadFile(examplesPath + "/chameleon_lime.jpg"); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
result, err := tensorFlow.Labels(imageBuffer)
|
||||
|
@ -108,7 +108,7 @@ func TestTensorFlow_Labels(t *testing.T) {
|
|||
t.Run("dog_orange.jpg", func(t *testing.T) {
|
||||
tensorFlow := NewTest(t)
|
||||
|
||||
if imageBuffer, err := ioutil.ReadFile(examplesPath + "/dog_orange.jpg"); err != nil {
|
||||
if imageBuffer, err := os.ReadFile(examplesPath + "/dog_orange.jpg"); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
result, err := tensorFlow.Labels(imageBuffer)
|
||||
|
@ -132,7 +132,7 @@ func TestTensorFlow_Labels(t *testing.T) {
|
|||
t.Run("Random.docx", func(t *testing.T) {
|
||||
tensorFlow := NewTest(t)
|
||||
|
||||
if imageBuffer, err := ioutil.ReadFile(examplesPath + "/Random.docx"); err != nil {
|
||||
if imageBuffer, err := os.ReadFile(examplesPath + "/Random.docx"); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
result, err := tensorFlow.Labels(imageBuffer)
|
||||
|
@ -143,7 +143,7 @@ func TestTensorFlow_Labels(t *testing.T) {
|
|||
t.Run("6720px_white.jpg", func(t *testing.T) {
|
||||
tensorFlow := NewTest(t)
|
||||
|
||||
if imageBuffer, err := ioutil.ReadFile(examplesPath + "/6720px_white.jpg"); err != nil {
|
||||
if imageBuffer, err := os.ReadFile(examplesPath + "/6720px_white.jpg"); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
result, err := tensorFlow.Labels(imageBuffer)
|
||||
|
@ -158,7 +158,7 @@ func TestTensorFlow_Labels(t *testing.T) {
|
|||
t.Run("disabled true", func(t *testing.T) {
|
||||
tensorFlow := New(assetsPath, true)
|
||||
|
||||
if imageBuffer, err := ioutil.ReadFile(examplesPath + "/dog_orange.jpg"); err != nil {
|
||||
if imageBuffer, err := os.ReadFile(examplesPath + "/dog_orange.jpg"); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
result, err := tensorFlow.Labels(imageBuffer)
|
||||
|
@ -224,7 +224,7 @@ func TestTensorFlow_MakeTensor(t *testing.T) {
|
|||
t.Run("cat_brown.jpg", func(t *testing.T) {
|
||||
tensorFlow := NewTest(t)
|
||||
|
||||
imageBuffer, err := ioutil.ReadFile(examplesPath + "/cat_brown.jpg")
|
||||
imageBuffer, err := os.ReadFile(examplesPath + "/cat_brown.jpg")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -238,7 +238,7 @@ func TestTensorFlow_MakeTensor(t *testing.T) {
|
|||
t.Run("Random.docx", func(t *testing.T) {
|
||||
tensorFlow := NewTest(t)
|
||||
|
||||
imageBuffer, err := ioutil.ReadFile(examplesPath + "/Random.docx")
|
||||
imageBuffer, err := os.ReadFile(examplesPath + "/Random.docx")
|
||||
assert.Nil(t, err)
|
||||
result, err := tensorFlow.createTensor(imageBuffer, "jpeg")
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
@ -157,7 +156,7 @@ func backupAction(ctx *cli.Context) error {
|
|||
fmt.Println(out.String())
|
||||
} else {
|
||||
// Write output to file.
|
||||
if err := ioutil.WriteFile(indexFileName, []byte(out.String()), os.ModePerm); err != nil {
|
||||
if err := os.WriteFile(indexFileName, []byte(out.String()), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
|
@ -55,9 +57,7 @@ func cleanUpAction(ctx *cli.Context) error {
|
|||
if thumbs, orphans, err := w.Start(opt); err != nil {
|
||||
return err
|
||||
} else {
|
||||
elapsed := time.Since(start)
|
||||
|
||||
log.Infof("cleanup: removed %d index entries and %d orphan thumbnails in %s", orphans, thumbs, elapsed)
|
||||
log.Infof("removed %s and %s in %s", english.Plural(orphans, "index entry", "index entries"), english.Plural(thumbs, "thumbnail", "thumbnails"), time.Since(start))
|
||||
}
|
||||
|
||||
conf.Shutdown()
|
||||
|
|
|
@ -26,9 +26,12 @@ func configAction(ctx *cli.Context) error {
|
|||
|
||||
fmt.Printf("%-25s VALUE\n", "NAME")
|
||||
|
||||
// Feature flags.
|
||||
// Feature flags and auth.
|
||||
fmt.Printf("%-25s %t\n", "debug", conf.Debug())
|
||||
fmt.Printf("%-25s %s\n", "log-level", conf.LogLevel())
|
||||
fmt.Printf("%-25s %s\n", "log-filename", conf.LogFilename())
|
||||
fmt.Printf("%-25s %t\n", "public", conf.Public())
|
||||
fmt.Printf("%-25s %s\n", "admin-password", strings.Repeat("*", utf8.RuneCountInString(conf.AdminPassword())))
|
||||
fmt.Printf("%-25s %t\n", "read-only", conf.ReadOnly())
|
||||
fmt.Printf("%-25s %t\n", "experimental", conf.Experimental())
|
||||
|
||||
|
@ -84,29 +87,21 @@ func configAction(ctx *cli.Context) error {
|
|||
// Site information.
|
||||
fmt.Printf("%-25s %s\n", "site-url", conf.SiteUrl())
|
||||
fmt.Printf("%-25s %s\n", "site-preview", conf.SitePreview())
|
||||
fmt.Printf("%-25s %s\n", "site-author", conf.SiteAuthor())
|
||||
fmt.Printf("%-25s %s\n", "site-title", conf.SiteTitle())
|
||||
fmt.Printf("%-25s %s\n", "site-caption", conf.SiteCaption())
|
||||
fmt.Printf("%-25s %s\n", "site-description", conf.SiteDescription())
|
||||
fmt.Printf("%-25s %s\n", "site-author", conf.SiteAuthor())
|
||||
fmt.Printf("%-25s %s\n", "cdn-url", conf.CdnUrl("/"))
|
||||
fmt.Printf("%-25s %s\n", "content-uri", conf.ContentUri())
|
||||
fmt.Printf("%-25s %s\n", "static-uri", conf.StaticUri())
|
||||
fmt.Printf("%-25s %s\n", "api-uri", conf.ApiUri())
|
||||
fmt.Printf("%-25s %s\n", "base-uri", conf.BaseUri("/"))
|
||||
|
||||
// Logging.
|
||||
fmt.Printf("%-25s %s\n", "log-level", conf.LogLevel())
|
||||
fmt.Printf("%-25s %s\n", "log-filename", conf.LogFilename())
|
||||
fmt.Printf("%-25s %s\n", "pid-filename", conf.PIDFilename())
|
||||
|
||||
// HTTP server configuration.
|
||||
fmt.Printf("%-25s %s\n", "http-host", conf.HttpHost())
|
||||
fmt.Printf("%-25s %d\n", "http-port", conf.HttpPort())
|
||||
fmt.Printf("%-25s %s\n", "http-mode", conf.HttpMode())
|
||||
|
||||
// Passwords.
|
||||
fmt.Printf("%-25s %s\n", "admin-password", strings.Repeat("*", utf8.RuneCountInString(conf.AdminPassword())))
|
||||
|
||||
// Database configuration.
|
||||
fmt.Printf("%-25s %s\n", "database-driver", dbDriver)
|
||||
fmt.Printf("%-25s %s\n", "database-server", conf.DatabaseServer())
|
||||
|
@ -147,9 +142,14 @@ func configAction(ctx *cli.Context) error {
|
|||
fmt.Printf("%-25s %d\n", "face-size", conf.FaceSize())
|
||||
fmt.Printf("%-25s %f\n", "face-score", conf.FaceScore())
|
||||
fmt.Printf("%-25s %d\n", "face-overlap", conf.FaceOverlap())
|
||||
fmt.Printf("%-25s %d\n", "face-cluster-size", conf.FaceClusterSize())
|
||||
fmt.Printf("%-25s %d\n", "face-cluster-score", conf.FaceClusterScore())
|
||||
fmt.Printf("%-25s %d\n", "face-cluster-core", conf.FaceClusterCore())
|
||||
fmt.Printf("%-25s %f\n", "face-cluster-dist", conf.FaceClusterDist())
|
||||
fmt.Printf("%-25s %f\n", "face-match-dist", conf.FaceMatchDist())
|
||||
|
||||
// Other.
|
||||
fmt.Printf("%-25s %s\n", "pid-filename", conf.PIDFilename())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -18,6 +18,11 @@ func TestConfigCommand(t *testing.T) {
|
|||
err = ConfigCommand.Run(ctx)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Expected config command output.
|
||||
assert.Contains(t, output, "NAME VALUE")
|
||||
assert.Contains(t, output, "config-file")
|
||||
assert.Contains(t, output, "darktable-cli")
|
||||
|
@ -25,7 +30,4 @@ func TestConfigCommand(t *testing.T) {
|
|||
assert.Contains(t, output, "import-path")
|
||||
assert.Contains(t, output, "cache-path")
|
||||
assert.Contains(t, output, "assets-path")
|
||||
|
||||
assert.Equal(t, output, output)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ import (
|
|||
// ConvertCommand registers the convert cli command.
|
||||
var ConvertCommand = cli.Command{
|
||||
Name: "convert",
|
||||
Usage: "Converts originals in other formats to JPEG and AVC sidecar files",
|
||||
UsageText: `To limit scope, a sub folder may be passed as first argument.`,
|
||||
Usage: "Converts media files in other formats to JPEG / AVC",
|
||||
ArgsUsage: "[path]",
|
||||
Action: convertAction,
|
||||
}
|
||||
|
||||
|
|
|
@ -16,10 +16,11 @@ import (
|
|||
|
||||
// CopyCommand registers the copy cli command.
|
||||
var CopyCommand = cli.Command{
|
||||
Name: "copy",
|
||||
Aliases: []string{"cp"},
|
||||
Usage: "Copies files to originals folder, converts and indexes them as needed",
|
||||
Action: copyAction,
|
||||
Name: "cp",
|
||||
Aliases: []string{"copy"},
|
||||
Usage: "Copies media files to originals",
|
||||
ArgsUsage: "[path]",
|
||||
Action: copyAction,
|
||||
}
|
||||
|
||||
// copyAction copies photos to originals path. Default import path is used if no path argument provided
|
||||
|
@ -59,7 +60,7 @@ func copyAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
if sourcePath == conf.OriginalsPath() {
|
||||
return errors.New("import folder is identical with originals")
|
||||
return errors.New("import path is identical with originals")
|
||||
}
|
||||
|
||||
log.Infof("copying media files from %s to %s", sourcePath, conf.OriginalsPath())
|
||||
|
@ -72,6 +73,8 @@ func copyAction(ctx *cli.Context) error {
|
|||
elapsed := time.Since(start)
|
||||
|
||||
log.Infof("import completed in %s", elapsed)
|
||||
|
||||
conf.Shutdown()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/urfave/cli"
|
||||
|
||||
|
@ -17,10 +18,10 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// FacesCommand registers the faces cli command.
|
||||
// FacesCommand registers the facial recognition subcommands.
|
||||
var FacesCommand = cli.Command{
|
||||
Name: "faces",
|
||||
Usage: "Facial recognition sub-commands",
|
||||
Usage: "Facial recognition subcommands",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "stats",
|
||||
|
@ -33,14 +34,14 @@ var FacesCommand = cli.Command{
|
|||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "fix, f",
|
||||
Usage: "issues will be fixed automatically",
|
||||
Usage: "fix discovered issues",
|
||||
},
|
||||
},
|
||||
Action: facesAuditAction,
|
||||
},
|
||||
{
|
||||
Name: "reset",
|
||||
Usage: "Removes people and faces",
|
||||
Usage: "Removes people and faces after confirmation",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "force, f",
|
||||
|
@ -61,7 +62,7 @@ var FacesCommand = cli.Command{
|
|||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "force, f",
|
||||
Usage: "update all faces",
|
||||
Usage: "update all faces",
|
||||
},
|
||||
},
|
||||
Action: facesUpdateAction,
|
||||
|
@ -183,7 +184,7 @@ func facesResetAction(ctx *cli.Context) error {
|
|||
// facesResetAllAction removes all people, faces, and face markers.
|
||||
func facesResetAllAction(ctx *cli.Context) error {
|
||||
actionPrompt := promptui.Prompt{
|
||||
Label: "Permanently delete all people and faces?",
|
||||
Label: "Permanently remove all people and faces?",
|
||||
IsConfirm: true,
|
||||
}
|
||||
|
||||
|
@ -259,7 +260,9 @@ func facesIndexAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
indexed = w.Start(opt)
|
||||
} else if w := service.Purge(); w != nil {
|
||||
}
|
||||
|
||||
if w := service.Purge(); w != nil {
|
||||
opt := photoprism.PurgeOptions{
|
||||
Path: subPath,
|
||||
Ignore: indexed,
|
||||
|
@ -268,13 +271,13 @@ func facesIndexAction(ctx *cli.Context) error {
|
|||
if files, photos, err := w.Start(opt); err != nil {
|
||||
log.Error(err)
|
||||
} else if len(files) > 0 || len(photos) > 0 {
|
||||
log.Infof("purge: removed %d files and %d photos", len(files), len(photos))
|
||||
log.Infof("purge: removed %s and %s", english.Plural(len(files), "file", "files"), english.Plural(len(photos), "photo", "photos"))
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
|
||||
log.Infof("indexed %d files in %s", len(indexed), elapsed)
|
||||
log.Infof("indexed %s in %s", english.Plural(len(indexed), "file", "files"), elapsed)
|
||||
|
||||
conf.Shutdown()
|
||||
|
||||
|
@ -339,7 +342,7 @@ func facesOptimizeAction(ctx *cli.Context) error {
|
|||
} else {
|
||||
elapsed := time.Since(start)
|
||||
|
||||
log.Infof("%d face clusters merged in %s", res.Merged, elapsed)
|
||||
log.Infof("merged %s in %s", english.Plural(res.Merged, "face cluster", "face clusters"), elapsed)
|
||||
}
|
||||
|
||||
conf.Shutdown()
|
||||
|
|
|
@ -16,10 +16,11 @@ import (
|
|||
|
||||
// ImportCommand registers the import cli command.
|
||||
var ImportCommand = cli.Command{
|
||||
Name: "import",
|
||||
Aliases: []string{"mv"},
|
||||
Usage: "Moves files to originals folder, converts and indexes them as needed",
|
||||
Action: importAction,
|
||||
Name: "mv",
|
||||
Aliases: []string{"import"},
|
||||
Usage: "Moves media files to originals",
|
||||
ArgsUsage: "[path]",
|
||||
Action: importAction,
|
||||
}
|
||||
|
||||
// importAction moves photos to originals path. Default import path is used if no path argument provided
|
||||
|
@ -59,7 +60,7 @@ func importAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
if sourcePath == conf.OriginalsPath() {
|
||||
return errors.New("import folder is identical with originals")
|
||||
return errors.New("import path is identical with originals")
|
||||
}
|
||||
|
||||
log.Infof("moving media files from %s to %s", sourcePath, conf.OriginalsPath())
|
||||
|
@ -72,6 +73,8 @@ func importAction(ctx *cli.Context) error {
|
|||
elapsed := time.Since(start)
|
||||
|
||||
log.Infof("import completed in %s", elapsed)
|
||||
|
||||
conf.Shutdown()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
|
@ -18,7 +20,7 @@ import (
|
|||
// IndexCommand registers the index cli command.
|
||||
var IndexCommand = cli.Command{
|
||||
Name: "index",
|
||||
Usage: "indexes media files in the originals folder",
|
||||
Usage: "Indexes original media files",
|
||||
ArgsUsage: "[path]",
|
||||
Flags: indexFlags,
|
||||
Action: indexAction,
|
||||
|
@ -75,7 +77,10 @@ func indexAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
indexed = w.Start(opt)
|
||||
} else if w := service.Purge(); w != nil {
|
||||
}
|
||||
|
||||
if w := service.Purge(); w != nil {
|
||||
purgeStart := time.Now()
|
||||
opt := photoprism.PurgeOptions{
|
||||
Path: subPath,
|
||||
Ignore: indexed,
|
||||
|
@ -84,9 +89,12 @@ func indexAction(ctx *cli.Context) error {
|
|||
if files, photos, err := w.Start(opt); err != nil {
|
||||
log.Error(err)
|
||||
} else if len(files) > 0 || len(photos) > 0 {
|
||||
log.Infof("purge: removed %d files and %d photos", len(files), len(photos))
|
||||
log.Infof("purge: removed %s and %s [%s]", english.Plural(len(files), "file", "files"), english.Plural(len(photos), "photo", "photos"), time.Since(purgeStart))
|
||||
}
|
||||
} else if ctx.Bool("cleanup") {
|
||||
}
|
||||
|
||||
if ctx.Bool("cleanup") {
|
||||
cleanupStart := time.Now()
|
||||
w := service.CleanUp()
|
||||
|
||||
opt := photoprism.CleanUpOptions{
|
||||
|
@ -96,13 +104,13 @@ func indexAction(ctx *cli.Context) error {
|
|||
if thumbs, orphans, err := w.Start(opt); err != nil {
|
||||
return err
|
||||
} else {
|
||||
log.Infof("cleanup: removed %d index entries and %d orphan thumbnails", orphans, thumbs)
|
||||
log.Infof("cleanup: removed %s and %s [%s]", english.Plural(orphans, "index entry", "index entries"), english.Plural(thumbs, "thumbnail", "thumbnails"), time.Since(cleanupStart))
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
|
||||
log.Infof("indexed %d files in %s", len(indexed), elapsed)
|
||||
log.Infof("indexed %s in %s", english.Plural(len(indexed), "file", "files"), elapsed)
|
||||
|
||||
conf.Shutdown()
|
||||
|
||||
|
|
55
internal/commands/index_test.go
Normal file
55
internal/commands/index_test.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/leandro-lugaresi/hub"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/pkg/capture"
|
||||
)
|
||||
|
||||
func TestIndexCommand(t *testing.T) {
|
||||
var err error
|
||||
|
||||
ctx := config.CliTestContext()
|
||||
|
||||
s := event.Subscribe("log.info")
|
||||
defer event.Unsubscribe(s)
|
||||
logs := ""
|
||||
|
||||
assert.IsType(t, hub.Subscription{}, s)
|
||||
|
||||
go func() {
|
||||
for msg := range s.Receiver {
|
||||
logs += msg.Fields["message"].(string) + "\n"
|
||||
}
|
||||
}()
|
||||
|
||||
stdout := capture.Output(func() {
|
||||
err = IndexCommand.Run(ctx)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if stdout != "" {
|
||||
t.Errorf("unexpected stdout output: %s", stdout)
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
if output := logs; output != "" {
|
||||
// Expected index command output.
|
||||
assert.Contains(t, output, "indexing originals")
|
||||
assert.Contains(t, output, "classify: loading labels")
|
||||
assert.Contains(t, output, "index: found no .ppignore file")
|
||||
} else {
|
||||
t.Fatal("log output missing")
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue