1
0
Fork 0
mirror of https://github.com/YunoHost/apps.git synced 2024-09-03 20:06:07 +02:00

Merge branch 'master' into support_gitlab_autoupgrade

This commit is contained in:
Alexandre Aubin 2024-01-24 00:14:59 +01:00 committed by GitHub
commit 8a4789b68f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 990 additions and 982 deletions

View file

@ -67,3 +67,8 @@ App packagers should *not* manually set their apps' level. The levels of all the
Applications with no recent activity and no active sign from maintainer may be flagged in `apps.toml` with the `package-not-maintained` antifeature tag to signify that the app is inactive and may slowly become outdated with respect to the upstream, or with respect to good packaging practices. It does **not** mean that the app is not working anymore. Applications with no recent activity and no active sign from maintainer may be flagged in `apps.toml` with the `package-not-maintained` antifeature tag to signify that the app is inactive and may slowly become outdated with respect to the upstream, or with respect to good packaging practices. It does **not** mean that the app is not working anymore.
Feel free to contact the app group if you feel like taking over the maintenance of a currently unmaintained app! Feel free to contact the app group if you feel like taking over the maintenance of a currently unmaintained app!
### `graveyard.toml`
This file is for apps that are long-term not-working and unlikely to be ever revived

980
apps.toml

File diff suppressed because it is too large Load diff

257
graveyard.toml Normal file
View file

@ -0,0 +1,257 @@
[anfora]
category = "social_media"
subtags = [ "pictures" ]
url = "https://github.com/YunoHost-Apps/anfora_ynh"
[bibliogram]
category = "social_media"
potential_alternative_to = [ "Instagram" ]
subtags = [ "pictures" ]
url = "https://github.com/YunoHost-Apps/bibliogram_ynh"
[democracyos]
category = "communication"
subtags = [ "forum" ]
url = "https://github.com/YunoHost-Apps/democracyos_ynh"
[dockerui]
category = "system_tools"
url = "https://github.com/YunoHost-Apps/dockerui_ynh"
[dynette]
category = "wat"
url = "https://github.com/YunoHost-Apps/dynette_ynh"
[ecko]
category = "social_media"
subtags = [ "microblogging" ]
url = "https://github.com/YunoHost-Apps/ecko_ynh"
[fallback]
category = "system_tools"
subtags = [ "backup" ]
url = "https://github.com/YunoHost-Apps/fallback_ynh"
[flask]
category = "dev"
subtags = [ "skeleton" ]
url = "https://github.com/YunoHost-Apps/flask_ynh"
[flusio]
category = "reading"
subtags = [ "rssreader" ]
url = "https://github.com/YunoHost-Apps/flusio_ynh"
[foodsoft]
category = "productivity_and_management"
subtags = [ "business_and_ngos" ]
url = "https://github.com/YunoHost-Apps/foodsoft_ynh"
[framaestro]
category = "communication"
subtags = [ "meeting" ]
url = "https://github.com/YunoHost-Apps/framaestro_ynh"
[framaestro_hub]
category = "communication"
subtags = [ "meeting" ]
url = "https://github.com/YunoHost-Apps/framaestro_hub_ynh"
[freeboard]
category = "iot"
url = "https://github.com/YunoHost-Apps/freeboard_ynh"
[freepbx]
category = "communication"
url = "https://github.com/YunoHost-Apps/freepbx_ynh"
[ftp_webapp]
category = "small_utilities"
url = "https://github.com/YunoHost-Apps/ftp_support_webapp_ynh"
[ftssolr]
category = "wat"
url = "https://github.com/YunoHost-Apps/ftssolr_ynh"
[gekko]
category = "wat"
url = "https://github.com/YunoHost-Apps/gekko_ynh"
[gitrepositories]
category = "dev"
subtags = [ "forge" ]
url = "https://github.com/YunoHost-Apps/gitrepositories_ynh"
[gnusocial]
category = "social_media"
potential_alternative_to = [ "X" ]
subtags = [ "microblogging" ]
url = "https://github.com/YunoHost-Apps/gnusocial_ynh"
[gogswebhost]
category = "publishing"
url = "https://github.com/YunoHost-Apps/gogs_webhost_ynh"
[internetarchive]
category = "wat"
url = "https://github.com/YunoHost-Apps/internetarchive_ynh"
[jappix_mini]
category = "communication"
subtags = [ "chat" ]
url = "https://github.com/YunoHost-Apps/jappix_mini_ynh"
[lbcalerte]
category = "small_utilities"
url = "https://github.com/YunoHost-Apps/lbcalerte_ynh"
[lektor]
category = "publishing"
subtags = [ "website" ]
url = "https://github.com/YunoHost-Apps/lektor_ynh"
[mailman]
category = "communication"
potential_alternative_to = [ "Google Groups" ]
subtags = [ "email" ]
url = "https://github.com/yunohost-apps/mailman_ynh"
[mediadrop]
category = "multimedia"
subtags = [ "mediacenter" ]
url = "https://github.com/YunoHost-Apps/mediadrop_ynh"
[menu]
category = "wat"
url = "https://github.com/YunoHost-Apps/menu_ynh"
[modernpaste]
category = "small_utilities"
subtags = [ "pastebin" ]
url = "https://github.com/YunoHost-Apps/modernpaste_ynh"
[monit]
category = "system_tools"
subtags = [ "monitoring" ]
url = "https://github.com/YunoHost-Apps/monit_ynh"
[multi_webapp]
category = "publishing"
subtags = [ "website" ]
url = "https://github.com/YunoHost-Apps/multi_webapp_ynh"
[munin]
category = "system_tools"
subtags = [ "monitoring" ]
url = "https://github.com/YunoHost-Apps/munin_ynh"
[nexusoss]
category = "dev"
url = "https://github.com/YunoHost-Apps/nexusoss_ynh"
[ntopng]
category = "system_tools"
url = "https://github.com/YunoHost-Apps/ntopng_ynh"
[osmw]
category = "wat"
url = "https://github.com/YunoHost-Apps/osmw_ynh"
[peachpub]
category = "communication"
url = "https://github.com/YunoHost-Apps/peachpub_ynh"
[piratebox]
category = "system_tools"
subtags = [ "network" ]
url = "https://github.com/labriqueinternet/piratebox_ynh"
[plonecms]
category = "publishing"
subtags = [ "website" ]
url = "https://github.com/YunoHost-Apps/plonecms_ynh"
[portainer]
category = "system_tools"
url = "https://github.com/YunoHost-Apps/portainer_ynh"
[reel2bits]
category = "social_media"
potential_alternative_to = [ "Soundcloud" ]
subtags = [ "music" ]
url = "https://github.com/YunoHost-Apps/reel2bits_ynh"
[remotestorage]
category = "small_utilities"
url = "https://github.com/YunoHost-Apps/remotestorage_ynh"
[roadiz]
category = "publishing"
subtags = [ "website" ]
url = "https://github.com/YunoHost-Apps/roadiz_ynh"
[shsd]
category = "system_tools"
subtags = [ "monitoring" ]
url = "https://github.com/YunoHost-Apps/shsd_ynh"
[sickbeard]
category = "multimedia"
subtags = [ "download" ]
url = "https://github.com/YunoHost-Apps/sickbeard_ynh"
[sickrage]
category = "multimedia"
subtags = [ "download" ]
url = "https://github.com/YunoHost-Apps/sickrage_ynh"
[sonerezh]
category = "multimedia"
subtags = [ "music" ]
url = "https://github.com/YunoHost-Apps/sonerezh_ynh"
[staticwebapp]
category = "publishing"
url = "https://github.com/YunoHost-Apps/staticwebapp_ynh"
[subscribe]
category = "wat"
url = "https://github.com/YunoHost-Apps/subscribe_ynh"
[tagspaces]
category = "synchronization"
url = "https://github.com/YunoHost-Apps/tagspaces_ynh"
[telegram_chatbot]
category = "dev"
url = "https://github.com/YunoHost-Apps/telegram_chatbot_ynh"
[tes3mp]
category = "games"
url = "https://github.com/YunoHost-Apps/tes3mp_ynh"
[transpay]
category = "productivity_and_management"
url = "https://github.com/YunoHost-Apps/transpay_ynh"
[unbound]
category = "system_tools"
url = "https://github.com/YunoHost-Apps/unbound_ynh"
[vpnserver]
category = "system_tools"
subtags = [ "network" ]
url = "https://github.com/YunoHost-Apps/vpnserver_ynh"
[wildfly]
category = "dev"
url = "https://github.com/YunoHost-Apps/wildfly_ynh"
[youtube-dl-webui]
category = "multimedia"
subtags = [ "download" ]
url = "https://github.com/YunoHost-Apps/youtube-dl-webui_ynh"
[yunofav]
category = "wat"
url = "https://github.com/YunoHost-Apps/yunofav_ynh"

BIN
logos/diacamma.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
logos/fluffychat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
logos/freescout.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
logos/grist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
logos/joplin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
logos/petitesannonces.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
logos/planka.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
logos/simplytranslate.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
logos/terraforming-mars.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
logos/traccar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
logos/woodpecker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
logos/xwiki.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View file

@ -85,7 +85,7 @@
"version": { "version": {
"description": "App and package version", "description": "App and package version",
"type": "string", "type": "string",
"pattern": "^[0-9a-z]*([\\.-][0-9a-z]*)*~ynh[0-9]*$" "pattern": "^[0-9a-z]*([\\.+-][0-9a-z]*)*~ynh[0-9]*$"
}, },
"description": {"$ref": "#/$defs/translated_string"}, "description": {"$ref": "#/$defs/translated_string"},
"maintainers": { "maintainers": {
@ -371,7 +371,14 @@
}, },
"sha256": {"$ref": "#/$defs/sha256sum"}, "sha256": {"$ref": "#/$defs/sha256sum"},
"in_subdir": { "in_subdir": {
"type": "boolean" "anyOf": [
{
"type": "boolean"
},
{
"type": "integer"
}
]
}, },
"prefetch": { "prefetch": {
"type": "boolean" "type": "boolean"

View file

@ -31,6 +31,8 @@ from utils import (
get_wishlist, get_wishlist,
get_stars, get_stars,
get_app_md_and_screenshots, get_app_md_and_screenshots,
save_wishlist_submit_for_ratelimit,
check_wishlist_submit_ratelimit,
) )
app = Flask(__name__, static_url_path="/assets", static_folder="assets") app = Flask(__name__, static_url_path="/assets", static_folder="assets")
@ -147,7 +149,7 @@ def star_app(app_id, action):
if app_id not in get_catalog()["apps"] and app_id not in get_wishlist(): if app_id not in get_catalog()["apps"] and app_id not in get_wishlist():
return _("App %(app_id) not found", app_id=app_id), 404 return _("App %(app_id) not found", app_id=app_id), 404
if not session.get("user", {}): if not session.get("user", {}):
return _("You must be logged in to be able to star an app"), 401 return _("You must be logged in to be able to star an app") + "<br/><br/>" + _("Note that, due to various abuses, we restricted login on the app store to 'trust level 1' users.<br/><br/>'Trust level 1' is obtained after interacting a minimum with the forum, and more specifically: entering at least 5 topics, reading at least 30 posts, and spending at least 10 minutes reading posts."), 401
app_star_folder = os.path.join(".stars", app_id) app_star_folder = os.path.join(".stars", app_id)
app_star_for_this_user = os.path.join( app_star_for_this_user = os.path.join(
@ -190,7 +192,7 @@ def add_to_wishlist():
if request.method == "POST": if request.method == "POST":
user = session.get("user", {}) user = session.get("user", {})
if not user: if not user:
errormsg = _("You must be logged in to submit an app to the wishlist") errormsg = _("You must be logged in to submit an app to the wishlist") + "<br/><br/>" + _("Note that, due to various abuses, we restricted login on the app store to 'trust level 1' users.<br/><br/>'Trust level 1' is obtained after interacting a minimum with the forum, and more specifically: entering at least 5 topics, reading at least 30 posts, and spending at least 10 minutes reading posts.")
return render_template( return render_template(
"wishlist_add.html", "wishlist_add.html",
locale=get_locale(), locale=get_locale(),
@ -199,7 +201,6 @@ def add_to_wishlist():
successmsg=None, successmsg=None,
errormsg=errormsg, errormsg=errormsg,
) )
csrf_token = request.form["csrf_token"] csrf_token = request.form["csrf_token"]
if csrf_token != session.get("csrf_token"): if csrf_token != session.get("csrf_token"):
@ -217,8 +218,15 @@ def add_to_wishlist():
description = request.form["description"].strip().replace("\n", "") description = request.form["description"].strip().replace("\n", "")
upstream = request.form["upstream"].strip().replace("\n", "") upstream = request.form["upstream"].strip().replace("\n", "")
website = request.form["website"].strip().replace("\n", "") website = request.form["website"].strip().replace("\n", "")
license = request.form["license"].strip().replace("\n", "")
boring_keywords_to_check_for_people_not_reading_the_instructions = ["free", "open source", "open-source", "self-hosted", "simple", "lightweight", "light-weight", "best", "most", "fast", "flexible", "puissante", "powerful", "secure"]
checks = [ checks = [
(
check_wishlist_submit_ratelimit(session['user']['username']) is True,
_("Proposing wishlist additions is limited to once every 15 days per user.")
),
(len(name) >= 3, _("App name should be at least 3 characters")), (len(name) >= 3, _("App name should be at least 3 characters")),
(len(name) <= 30, _("App name should be less than 30 characters")), (len(name) <= 30, _("App name should be less than 30 characters")),
( (
@ -237,11 +245,27 @@ def add_to_wishlist():
len(upstream) <= 150, len(upstream) <= 150,
_("Upstream code repo URL should be less than 150 characters"), _("Upstream code repo URL should be less than 150 characters"),
), ),
(
len(license) >= 10,
_("License URL should be at least 10 characters"),
),
(
len(license) <= 250,
_("License URL should be less than 250 characters"),
),
(len(website) <= 150, _("Website URL should be less than 150 characters")), (len(website) <= 150, _("Website URL should be less than 150 characters")),
( (
re.match(r"^[\w\.\-\(\)\ ]+$", name), re.match(r"^[\w\.\-\(\)\ ]+$", name),
_("App name contains special characters"), _("App name contains special characters"),
), ),
(
all(keyword not in description.lower() for keyword in boring_keywords_to_check_for_people_not_reading_the_instructions),
_("Please focus on what the app does, without using marketing, fuzzy terms, or repeating that the app is 'free' and 'self-hostable'.")
),
(
description.lower().split()[0] != name and (len(description.split()) == 1 or description.lower().split()[1] not in ["is", "est"]),
_("No need to repeat '{app} is'. Focus on what the app does.")
)
] ]
for check, errormsg in checks: for check, errormsg in checks:
@ -328,6 +352,7 @@ Proposed by **{session['user']['username']}**
Website: {website} Website: {website}
Upstream repo: {upstream} Upstream repo: {upstream}
License: {license}
Description: {description} Description: {description}
- [ ] Confirm app is self-hostable and generally makes sense to possibly integrate in YunoHost - [ ] Confirm app is self-hostable and generally makes sense to possibly integrate in YunoHost
@ -349,6 +374,9 @@ Description: {description}
"Your proposed app has succesfully been submitted. It must now be validated by the YunoHost team. You can track progress here: <a href='%(url)s'>%(url)s</a>", "Your proposed app has succesfully been submitted. It must now be validated by the YunoHost team. You can track progress here: <a href='%(url)s'>%(url)s</a>",
url=url, url=url,
) )
save_wishlist_submit_for_ratelimit(session['user']['username'])
return render_template( return render_template(
"wishlist_add.html", "wishlist_add.html",
locale=get_locale(), locale=get_locale(),
@ -414,6 +442,9 @@ def sso_login_callback():
uri_to_redirect_to_after_login = session.get("uri_to_redirect_to_after_login") uri_to_redirect_to_after_login = session.get("uri_to_redirect_to_after_login")
if "trust_level_1" not in user_data['groups'][0].split(','):
return _("Unfortunately, login was denied.") + "<br/><br/>" + _("Note that, due to various abuses, we restricted login on the app store to 'trust level 1' users.<br/><br/>'Trust level 1' is obtained after interacting a minimum with the forum, and more specifically: entering at least 5 topics, reading at least 30 posts, and spending at least 10 minutes reading posts."), 403
session.clear() session.clear()
session["user"] = { session["user"] = {
"id": user_data["external_id"][0], "id": user_data["external_id"][0],

View file

@ -27,20 +27,24 @@
<p class="mt-2 text-sm text-orange-700 font-bold"> <p class="mt-2 text-sm text-orange-700 font-bold">
<i class="fa fa-exclamation-triangle fa-fw" aria-hidden="true"></i> <i class="fa fa-exclamation-triangle fa-fw" aria-hidden="true"></i>
{{ _("You must first login to be allowed to submit an app to the wishlist") }} {{ _("You must first login to be allowed to submit an app to the wishlist") }}
<br/><br/>
</p>
<p class="mt-2 text-sm text-orange-700">
{{ _("Note that, due to various abuses, we restricted login on the app store to 'trust level 1' users.<br/><br/>'Trust level 1' is obtained after interacting a minimum with the forum, and more specifically: entering at least 5 topics, reading at least 30 posts, and spending at least 10 minutes reading posts.") }}
</p>
</div>
{% else %}
<div role="alert" class="rounded-md border-s-4 border-orange-500 bg-orange-50 p-4 mb-5">
<p class="mt-2 text-sm text-orange-700 font-bold">
<i class="fa fa-exclamation-triangle fa-fw" aria-hidden="true"></i>
{{ _("Due to abuses, only one proposal every 15 days per user is allowed.") }}
</p>
<p class="mt-2 text-sm text-orange-700">
{{ _("Reviewing those proposals is tiring for volunteers, please don't yolo-send every random nerdy stuff you find on the Internet.") }}
</p> </p>
</div> </div>
{% endif %} {% endif %}
<div role="alert" class="rounded-md border-s-4 border-sky-500 bg-sky-50 p-4">
<p class="mt-2 text-sm text-sky-700 font-bold">
<i class="fa fa-info-circle fa-fw" aria-hidden="true"></i>
{{ _("Please check the license of the app your are proposing") }}
</p>
<p class="mt-2 text-sm text-sky-700">
{{ _("The YunoHost project will only package free/open-source software (with possible case-by-case exceptions for apps which are not-totally-free)") }}
</p>
</div>
{% if errormsg %} {% if errormsg %}
<div role="alert" class="rounded-md border-s-4 border-red-500 bg-red-50 p-4 my-5"> <div role="alert" class="rounded-md border-s-4 border-red-500 bg-red-50 p-4 my-5">
<p class="mt-2 text-sm text-red-700 font-bold"> <p class="mt-2 text-sm text-red-700 font-bold">
@ -64,6 +68,10 @@
<label for="upstream" class="mt-5 block font-bold text-gray-700">{{ _("Project code repository") }}</label> <label for="upstream" class="mt-5 block font-bold text-gray-700">{{ _("Project code repository") }}</label>
<input name="upstream" type="url" class="w-full mt-1 rounded-md border-gray-200 text-gray-700 shadow-sm" maxlength="150" required > <input name="upstream" type="url" class="w-full mt-1 rounded-md border-gray-200 text-gray-700 shadow-sm" maxlength="150" required >
<label for="license" class="mt-5 block font-bold text-gray-700">{{ _("Link to the project's LICENSE") }}</label>
<input name="license" type="url" class="w-full mt-1 rounded-md border-gray-200 text-gray-700 shadow-sm" required maxlength="250"></input>
<span class="text-xs text-gray-600 font-bold">{{ _("The YunoHost project will only package free/open-source software (with possible case-by-case exceptions for apps which are not-totally-free)") }}</span>
<label for="website" class="mt-5 block font-bold text-gray-700">{{ _("Project website") }}</label> <label for="website" class="mt-5 block font-bold text-gray-700">{{ _("Project website") }}</label>
<input name="website" type="url" class="w-full mt-1 rounded-md border-gray-200 text-gray-700 shadow-sm" maxlength="150" > <input name="website" type="url" class="w-full mt-1 rounded-md border-gray-200 text-gray-700 shadow-sm" maxlength="150" >
<span class="text-xs text-gray-600">{{ _("Please *do not* just copy-paste the code repository URL. If the project has no proper website, then leave the field empty.") }}</span> <span class="text-xs text-gray-600">{{ _("Please *do not* just copy-paste the code repository URL. If the project has no proper website, then leave the field empty.") }}</span>

View file

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2023-09-19 17:04+0200\n" "POT-Creation-Date: 2023-11-19 19:39+0100\n"
"PO-Revision-Date: 2023-09-05 19:50+0200\n" "PO-Revision-Date: 2023-09-05 19:50+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fr\n" "Language: fr\n"
@ -18,59 +18,73 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.12.1\n" "Generated-By: Babel 2.12.1\n"
#: app.py:140 #: app.py:148
msgid "App %(app_id) not found" msgid "App %(app_id) not found"
msgstr "L'app %(app_id) n'a pas été trouvée" msgstr "L'app %(app_id) n'a pas été trouvée"
#: app.py:142 #: app.py:150
msgid "You must be logged in to be able to star an app" msgid "You must be logged in to be able to star an app"
msgstr "Vous devez être connecté·e pour mettre une app en favoris" msgstr "Vous devez être connecté·e pour mettre une app en favoris"
#: app.py:185 #: app.py:150 app.py:193 app.py:418 templates/wishlist_add.html:31
msgid ""
"Note that, due to various abuses, we restricted login on the app store to"
" 'trust level 1' users.<br/><br/>'Trust level 1' is obtained after "
"interacting a minimum with the forum, and more specifically: entering at "
"least 5 topics, reading at least 30 posts, and spending at least 10 "
"minutes reading posts."
msgstr ""
"Notez que, suite à divers abus, la connexion nécessite maintenant d'être"
" 'trust level 1' sur le forum.<br/><br/>Le 'trust level 1' est obtenu après"
" avoir intéragit un minimum avec le forum, et plus précisémment: ouvrir au"
" moins 5 fils de discussion, lire au moins 30 messages, et passer au moins"
" 10 minutes à lire des messages."
#: app.py:193
msgid "You must be logged in to submit an app to the wishlist" msgid "You must be logged in to submit an app to the wishlist"
msgstr "Vous devez être connecté·e pour proposer une app pour la liste de souhaits" msgstr "Vous devez être connecté·e pour proposer une app pour la liste de souhaits"
#: app.py:200 #: app.py:206
msgid "Invalid CSRF token, please refresh the form and try again" msgid "Invalid CSRF token, please refresh the form and try again"
msgstr "Jeton CSRF invalide, prière de rafraîchir la page et retenter" msgstr "Jeton CSRF invalide, prière de rafraîchir la page et retenter"
#: app.py:216 #: app.py:222
msgid "App name should be at least 3 characters" msgid "App name should be at least 3 characters"
msgstr "Le nom d'app devrait contenir au moins 3 caractères" msgstr "Le nom d'app devrait contenir au moins 3 caractères"
#: app.py:217 #: app.py:223
msgid "App name should be less than 30 characters" msgid "App name should be less than 30 characters"
msgstr "Le nom d'app devrait contenir moins de 30 caractères" msgstr "Le nom d'app devrait contenir moins de 30 caractères"
#: app.py:220 #: app.py:226
msgid "App description should be at least 5 characters" msgid "App description should be at least 5 characters"
msgstr "La description de l'app devrait contenir au moins 5 caractères" msgstr "La description de l'app devrait contenir au moins 5 caractères"
#: app.py:224 #: app.py:230
msgid "App description should be less than 100 characters" msgid "App description should be less than 100 characters"
msgstr "La description de l'app devrait contenir moins de 100 caractères" msgstr "La description de l'app devrait contenir moins de 100 caractères"
#: app.py:228 #: app.py:234
msgid "Upstream code repo URL should be at least 10 characters" msgid "Upstream code repo URL should be at least 10 characters"
msgstr "L'URL du dépôt de code devrait contenir au moins 10 caractères" msgstr "L'URL du dépôt de code devrait contenir au moins 10 caractères"
#: app.py:232 #: app.py:238
msgid "Upstream code repo URL should be less than 150 characters" msgid "Upstream code repo URL should be less than 150 characters"
msgstr "L'URL du dépôt de code devrait contenir moins de 150 caractères" msgstr "L'URL du dépôt de code devrait contenir moins de 150 caractères"
#: app.py:234 #: app.py:240
msgid "Website URL should be less than 150 characters" msgid "Website URL should be less than 150 characters"
msgstr "L'URL du site web devrait contenir moins de 150 caractères" msgstr "L'URL du site web devrait contenir moins de 150 caractères"
#: app.py:237 #: app.py:243
msgid "App name contains special characters" msgid "App name contains special characters"
msgstr "Le nom de l'app contiens des caractères spéciaux" msgstr "Le nom de l'app contiens des caractères spéciaux"
#: app.py:270 #: app.py:276
msgid "An entry with the name %(slug) already exists in the wishlist" msgid "An entry with the name %(slug) already exists in the wishlist"
msgstr "Une entrée nommée $(slug) existe déjà dans la liste de souhaits" msgstr "Une entrée nommée $(slug) existe déjà dans la liste de souhaits"
#: app.py:295 #: app.py:299
msgid "" msgid ""
"Failed to create the pull request to add the app to the wishlist ... " "Failed to create the pull request to add the app to the wishlist ... "
"please report the issue to the yunohost team" "please report the issue to the yunohost team"
@ -78,15 +92,19 @@ msgstr ""
"Échec de la création de la demande d'intégration de l'app dans la liste " "Échec de la création de la demande d'intégration de l'app dans la liste "
"de souhaits ... merci de rapport le problème à l'équipe YunoHost" "de souhaits ... merci de rapport le problème à l'équipe YunoHost"
#: app.py:340 #: app.py:348
#, python-format
msgid "" msgid ""
"Your proposed app has succesfully been submitted. It must now be " "Your proposed app has succesfully been submitted. It must now be "
"validated by the YunoHost team. You can track progress here: %(url)s" "validated by the YunoHost team. You can track progress here: <a "
"href='%(url)s'>%(url)s</a>"
msgstr "" msgstr ""
"Un demande d'intégration à la liste de souhaits a bien été créée pour " "Un demande d'intégration à la liste de souhaits a bien été créée pour "
"cette app. Elle doit maintenant être validée par l'équipe YunoHost. Vous " "cette app. Elle doit maintenant être validée par l'équipe YunoHost. Vous "
"pouvez suivre cette demande ici: %(url)s" "pouvez suivre cette demande ici: <a href='%(url)s'>%(url)s</a>"
#: app.py:418
msgid "Unfortunately, login was denied."
msgstr "Malheureusement, la connexion a été refusée."
#: templates/app.html:10 templates/catalog.html:23 #: templates/app.html:10 templates/catalog.html:23
#, python-format #, python-format
@ -116,7 +134,9 @@ msgstr ""
msgid "" msgid ""
"This app has been good quality according to our automatic tests over at " "This app has been good quality according to our automatic tests over at "
"least one year." "least one year."
msgstr "Cette app est de bonne qualité d'après nos tests automatisés depuis au moins un an." msgstr ""
"Cette app est de bonne qualité d'après nos tests automatisés depuis au "
"moins un an."
#: templates/app.html:81 #: templates/app.html:81
msgid "Try the demo" msgid "Try the demo"
@ -204,34 +224,45 @@ msgstr "Dépôt de code du paquet YunoHost"
msgid "YunoHost app store" msgid "YunoHost app store"
msgstr "Store d'apps de YunoHost" msgstr "Store d'apps de YunoHost"
#: templates/base.html:56 templates/base.html:149 templates/index.html:3 #: templates/base.html:18 templates/base.html:113 templates/index.html:3
msgid "Home" msgid "Home"
msgstr "Accueil" msgstr "Accueil"
#: templates/base.html:65 templates/base.html:158 #: templates/base.html:27 templates/base.html:122
msgid "Catalog" msgid "Catalog"
msgstr "Catalogue" msgstr "Catalogue"
#: templates/base.html:71 templates/base.html:167 #: templates/base.html:33 templates/base.html:131
msgid "Wishlist" msgid "Wishlist"
msgstr "Liste de souhaits" msgstr "Liste de souhaits"
#: templates/base.html:84 templates/base.html:177 #: templates/base.html:46 templates/base.html:141
msgid "YunoHost documentation" msgid "YunoHost documentation"
msgstr "Documentation YunoHost" msgstr "Documentation YunoHost"
#: templates/base.html:92 templates/base.html:187 #: templates/base.html:54 templates/base.html:151
msgid "Login using YunoHost's forum" msgid "Login using YunoHost's forum"
msgstr "Se connecter via le forum YunoHost" msgstr "Se connecter via le forum YunoHost"
#: templates/base.html:122 templates/base.html:213 #: templates/base.html:86 templates/base.html:179
msgid "Logout" msgid "Logout"
msgstr "Se déconnecter" msgstr "Se déconnecter"
#: templates/base.html:135 #: templates/base.html:99
msgid "Toggle menu" msgid "Toggle menu"
msgstr "Activer le menu" msgstr "Activer le menu"
#: templates/base.html:197
msgid ""
"Made with <i class='text-red-500 fa fa-heart-o' aria-label='love'></i> "
"using <a class='text-blue-800' "
"href='https://flask.palletsprojects.com'>Flask</a> and <a class='text-"
"blue-800' href='https://tailwindcss.com/'>TailwindCSS</a> - <a class"
"='text-blue-800' "
"href='https://github.com/YunoHost/apps/tree/master/store'><i class='fa "
"fa-code fa-fw' aria-hidden='true'></i> Source</a>"
msgstr ""
#: templates/catalog.html:75 templates/catalog.html:80 #: templates/catalog.html:75 templates/catalog.html:80
msgid "Application Catalog" msgid "Application Catalog"
msgstr "Catalogue d'applications" msgstr "Catalogue d'applications"
@ -253,17 +284,17 @@ msgid "Sort by"
msgstr "Trier par" msgstr "Trier par"
#: templates/catalog.html:123 templates/wishlist.html:45 #: templates/catalog.html:123 templates/wishlist.html:45
msgid "Alphabetical" #: templates/wishlist.html:78
msgstr "Alphabétique" msgid "Popularity"
msgstr "Popularité"
#: templates/catalog.html:124 #: templates/catalog.html:124
msgid "Newest" msgid "Newest"
msgstr "Nouveauté" msgstr "Nouveauté"
#: templates/catalog.html:125 templates/wishlist.html:46 #: templates/catalog.html:125 templates/wishlist.html:46
#: templates/wishlist.html:78 msgid "Alphabetical"
msgid "Popularity" msgstr "Alphabétique"
msgstr "Popularité"
#: templates/catalog.html:128 templates/wishlist.html:49 #: templates/catalog.html:128 templates/wishlist.html:49
msgid "Requires to be logged-in" msgid "Requires to be logged-in"
@ -274,7 +305,7 @@ msgstr "Nécessite d'être connecté·e"
msgid "Show only apps you starred" msgid "Show only apps you starred"
msgstr "Montrer uniquement mes favoris" msgstr "Montrer uniquement mes favoris"
#: templates/catalog.html:155 templates/wishlist.html:152 #: templates/catalog.html:155 templates/wishlist.html:154
msgid "No results found." msgid "No results found."
msgstr "Aucun résultat trouvé." msgstr "Aucun résultat trouvé."
@ -326,7 +357,7 @@ msgstr ""
msgid "Suggest an app" msgid "Suggest an app"
msgstr "Suggérer une app" msgstr "Suggérer une app"
#: templates/wishlist.html:71 templates/wishlist_add.html:57 #: templates/wishlist.html:71 templates/wishlist_add.html:59
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
@ -338,11 +369,11 @@ msgstr "Description"
msgid "Official website" msgid "Official website"
msgstr "Site officiel" msgstr "Site officiel"
#: templates/wishlist.html:114 templates/wishlist.html:115 #: templates/wishlist.html:115 templates/wishlist.html:116
msgid "Code repository" msgid "Code repository"
msgstr "Dépôt de code officiel" msgstr "Dépôt de code officiel"
#: templates/wishlist.html:127 templates/wishlist.html:128 #: templates/wishlist.html:129 templates/wishlist.html:130
msgid "Star this app" msgid "Star this app"
msgstr "Étoiler cette app" msgstr "Étoiler cette app"
@ -354,11 +385,11 @@ msgstr "Suggérer une application à ajouter dans le catalogue de YunoHost"
msgid "You must first login to be allowed to submit an app to the wishlist" msgid "You must first login to be allowed to submit an app to the wishlist"
msgstr "Vous devez être connecté·e pour proposer une app pour la liste de souhaits" msgstr "Vous devez être connecté·e pour proposer une app pour la liste de souhaits"
#: templates/wishlist_add.html:37 #: templates/wishlist_add.html:39
msgid "Please check the license of the app your are proposing" msgid "Please check the license of the app your are proposing"
msgstr "Merci de vérifier la licence de l'app que vous proposez" msgstr "Merci de vérifier la licence de l'app que vous proposez"
#: templates/wishlist_add.html:40 #: templates/wishlist_add.html:42
msgid "" msgid ""
"The YunoHost project will only package free/open-source software (with " "The YunoHost project will only package free/open-source software (with "
"possible case-by-case exceptions for apps which are not-totally-free)" "possible case-by-case exceptions for apps which are not-totally-free)"
@ -367,15 +398,15 @@ msgstr ""
"(avec quelques possibles exceptions au cas-par-cas pour des apps qui ne " "(avec quelques possibles exceptions au cas-par-cas pour des apps qui ne "
"sont pas entièrement libres)" "sont pas entièrement libres)"
#: templates/wishlist_add.html:60 #: templates/wishlist_add.html:62
msgid "App's description" msgid "App's description"
msgstr "Description de l'app" msgstr "Description de l'app"
#: templates/wishlist_add.html:62 #: templates/wishlist_add.html:64
msgid "Please be concise and focus on what the app does." msgid "Please be concise and focus on what the app does."
msgstr "Prière de rester concis et de se concentrer sur ce que l'app fait." msgstr "Prière de rester concis et de se concentrer sur ce que l'app fait."
#: templates/wishlist_add.html:62 #: templates/wishlist_add.html:64
msgid "" msgid ""
"No need to repeat '[App] is ...'. No need to state that it is free/open-" "No need to repeat '[App] is ...'. No need to state that it is free/open-"
"source or self-hosted (otherwise it wouldn't be packaged for YunoHost). " "source or self-hosted (otherwise it wouldn't be packaged for YunoHost). "
@ -387,15 +418,15 @@ msgstr ""
"Évitez les formulations marketing type 'le meilleur', ou les propriétés " "Évitez les formulations marketing type 'le meilleur', ou les propriétés "
"vagues telles que 'facile', 'simple', 'léger'." "vagues telles que 'facile', 'simple', 'léger'."
#: templates/wishlist_add.html:64 #: templates/wishlist_add.html:66
msgid "Project code repository" msgid "Project code repository"
msgstr "Dépôt de code officiel" msgstr "Dépôt de code officiel"
#: templates/wishlist_add.html:67 #: templates/wishlist_add.html:69
msgid "Project website" msgid "Project website"
msgstr "Site officiel" msgstr "Site officiel"
#: templates/wishlist_add.html:69 #: templates/wishlist_add.html:71
msgid "" msgid ""
"Please *do not* just copy-paste the code repository URL. If the project " "Please *do not* just copy-paste the code repository URL. If the project "
"has no proper website, then leave the field empty." "has no proper website, then leave the field empty."
@ -403,7 +434,7 @@ msgstr ""
"Prière de *ne pas* juste copier-coller l'URL du dépôt de code. Si le " "Prière de *ne pas* juste copier-coller l'URL du dépôt de code. Si le "
"projet n'a pas de vrai site web, laissez le champ vide." "projet n'a pas de vrai site web, laissez le champ vide."
#: templates/wishlist_add.html:76 #: templates/wishlist_add.html:78
msgid "Submit" msgid "Submit"
msgstr "Envoyer" msgstr "Envoyer"

View file

@ -1,3 +1,4 @@
import time
import base64 import base64
import os import os
import json import json
@ -6,7 +7,7 @@ import subprocess
import pycmarkgfm import pycmarkgfm
from emoji import emojize from emoji import emojize
from flask import request from flask import request
from hashlib import md5
AVAILABLE_LANGUAGES = ["en"] + os.listdir("translations") AVAILABLE_LANGUAGES = ["en"] + os.listdir("translations")
@ -92,6 +93,26 @@ def get_stars():
get_stars.cache_checksum = None get_stars.cache_checksum = None
get_stars() get_stars()
def check_wishlist_submit_ratelimit(user):
dir_ = os.path.join(".wishlist_ratelimit")
if not os.path.exists(dir_):
os.mkdir(dir_)
f = os.path.join(dir_, md5(user.encode()).hexdigest())
return not os.path.exists(f) or (time.time() - os.path.getmtime(f)) > (15 * 24 * 3600) # 15 days
def save_wishlist_submit_for_ratelimit(user):
dir_ = os.path.join(".wishlist_ratelimit")
if not os.path.exists(dir_):
os.mkdir(dir_)
f = os.path.join(dir_, md5(user.encode()).hexdigest())
open(f, "w").write("")
def human_to_binary(size: str) -> int: def human_to_binary(size: str) -> int:
symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y") symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y")

View file

@ -47,7 +47,6 @@ else:
def apps_to_run_auto_update_for(): def apps_to_run_auto_update_for():
catalog = toml.load(open(os.path.dirname(__file__) + "/../../apps.toml")) catalog = toml.load(open(os.path.dirname(__file__) + "/../../apps.toml"))
apps_flagged_as_working_and_on_yunohost_apps_org = [ apps_flagged_as_working_and_on_yunohost_apps_org = [
@ -105,7 +104,6 @@ def filter_and_get_latest_tag(tags, app_id):
def tag_to_int_tuple(tag): def tag_to_int_tuple(tag):
tag = tag.strip("v").strip(".") tag = tag.strip("v").strip(".")
int_tuple = tag.split(".") int_tuple = tag.split(".")
assert all(i.isdigit() for i in int_tuple), f"Cant convert {tag} to int tuple :/" assert all(i.isdigit() for i in int_tuple), f"Cant convert {tag} to int tuple :/"
@ -127,7 +125,6 @@ def sha256_of_remote_file(url):
class AppAutoUpdater: class AppAutoUpdater:
def __init__(self, app_id, app_id_is_local_app_dir=False): def __init__(self, app_id, app_id_is_local_app_dir=False):
if app_id_is_local_app_dir: if app_id_is_local_app_dir:
if not os.path.exists(app_id + "/manifest.toml"): if not os.path.exists(app_id + "/manifest.toml"):
raise Exception("manifest.toml doesnt exists?") raise Exception("manifest.toml doesnt exists?")
@ -158,11 +155,9 @@ class AppAutoUpdater:
self.main_upstream = manifest.get("upstream", {}).get("code") self.main_upstream = manifest.get("upstream", {}).get("code")
def run(self): def run(self):
todos = {} todos = {}
for source, infos in self.sources.items(): for source, infos in self.sources.items():
if "autoupdate" not in infos: if "autoupdate" not in infos:
continue continue
@ -176,9 +171,16 @@ class AppAutoUpdater:
print(f"\n Checking {source} ...") print(f"\n Checking {source} ...")
new_version, new_asset_urls = self.get_latest_version_and_asset( if strategy == "latest_github_release":
strategy, asset, infos, source (
) new_version,
new_asset_urls,
changelog_url,
) = self.get_latest_version_and_asset(strategy, asset, infos, source)
else:
(new_version, new_asset_urls) = self.get_latest_version_and_asset(
strategy, asset, infos, source
)
if source == "main": if source == "main":
print(f"Current version in manifest: {self.current_version}") print(f"Current version in manifest: {self.current_version}")
@ -190,8 +192,12 @@ class AppAutoUpdater:
# Though we wrap this in a try/except pass, because don't want to miserably crash # Though we wrap this in a try/except pass, because don't want to miserably crash
# if the tag can't properly be converted to int tuple ... # if the tag can't properly be converted to int tuple ...
try: try:
if tag_to_int_tuple(self.current_version) > tag_to_int_tuple(new_version): if tag_to_int_tuple(self.current_version) > tag_to_int_tuple(
print("Up to date (current version appears more recent than newest version found)") new_version
):
print(
"Up to date (current version appears more recent than newest version found)"
)
continue continue
except: except:
pass pass
@ -200,9 +206,15 @@ class AppAutoUpdater:
print("Up to date") print("Up to date")
continue continue
if (isinstance(new_asset_urls, dict) and isinstance(infos.get("url"), str)) \ if (
or (isinstance(new_asset_urls, str) and not isinstance(infos.get("url"), str)): isinstance(new_asset_urls, dict) and isinstance(infos.get("url"), str)
raise Exception(f"It looks like there's an inconsistency between the old asset list and the new ones ... one is arch-specific, the other is not ... Did you forget to define arch-specific regexes ? ... New asset url is/are : {new_asset_urls}") ) or (
isinstance(new_asset_urls, str)
and not isinstance(infos.get("url"), str)
):
raise Exception(
f"It looks like there's an inconsistency between the old asset list and the new ones ... one is arch-specific, the other is not ... Did you forget to define arch-specific regexes ? ... New asset url is/are : {new_asset_urls}"
)
if isinstance(new_asset_urls, str) and infos["url"] == new_asset_urls: if isinstance(new_asset_urls, str) and infos["url"] == new_asset_urls:
print(f"URL for asset {source} is up to date") print(f"URL for asset {source} is up to date")
@ -226,11 +238,15 @@ class AppAutoUpdater:
return bool(todos) return bool(todos)
if "main" in todos: if "main" in todos:
if strategy == "latest_github_release":
title = f"Upgrade to v{new_version}"
message = f"Upgrade to v{new_version}\nChangelog: {changelog_url}"
else:
title = message = f"Upgrade to v{new_version}"
new_version = todos["main"]["new_version"] new_version = todos["main"]["new_version"]
message = f"Upgrade to v{new_version}"
new_branch = f"ci-auto-update-{new_version}" new_branch = f"ci-auto-update-{new_version}"
else: else:
message = "Upgrade sources" title = message = "Upgrade sources"
new_branch = "ci-auto-update-sources" new_branch = "ci-auto-update-sources"
try: try:
@ -265,7 +281,7 @@ class AppAutoUpdater:
# Open the PR # Open the PR
pr = self.repo.create_pull( pr = self.repo.create_pull(
title=message, body=message, head=new_branch, base=self.base_branch title=title, body=message, head=new_branch, base=self.base_branch
) )
print("Created PR " + self.repo.full_name + " updated with PR #" + str(pr.id)) print("Created PR " + self.repo.full_name + " updated with PR #" + str(pr.id))
@ -273,12 +289,13 @@ class AppAutoUpdater:
return bool(todos) return bool(todos)
def get_latest_version_and_asset(self, strategy, asset, infos, source): def get_latest_version_and_asset(self, strategy, asset, infos, source):
upstream = (
upstream = infos.get("autoupdate", {}).get("upstream", self.main_upstream).strip("/") infos.get("autoupdate", {}).get("upstream", self.main_upstream).strip("/")
)
if "github" in strategy: if "github" in strategy:
assert upstream and upstream.startswith( assert (
"https://github.com/" upstream and upstream.startswith("https://github.com/")
), f"When using strategy {strategy}, having a defined upstream code repo on github.com is required" ), f"When using strategy {strategy}, having a defined upstream code repo on github.com is required"
api = GithubAPI(upstream, auth=auth) api = GithubAPI(upstream, auth=auth)
elif "gitlab" in strategy: elif "gitlab" in strategy:
@ -294,24 +311,24 @@ class AppAutoUpdater:
latest_version_orig, latest_version = filter_and_get_latest_tag( latest_version_orig, latest_version = filter_and_get_latest_tag(
tags, self.app_id tags, self.app_id
) )
latest_release = [
release
for release in releases
if release["tag_name"] == latest_version_orig
][0]
latest_assets = {
a["name"]: a["browser_download_url"]
for a in latest_release["assets"]
if not a["name"].endswith(".md5")
}
latest_release_html_url = latest_release["html_url"]
if asset == "tarball": if asset == "tarball":
latest_tarball = ( latest_tarball = (
api.url_for_ref(latest_version_orig, RefType.tags) api.url_for_ref(latest_version_orig, RefType.tags)
) )
return latest_version, latest_tarball return latest_version, latest_tarball, latest_release_html_url
# FIXME # FIXME
else: else:
latest_release = [
release
for release in releases
if release["tag_name"] == latest_version_orig
][0]
latest_assets = {
a["name"]: a["browser_download_url"]
for a in latest_release["assets"]
if not a["name"].endswith(".md5")
}
latest_release_html_url = latest_release["html_url"]
if isinstance(asset, str): if isinstance(asset, str):
matching_assets_urls = [ matching_assets_urls = [
url url
@ -326,7 +343,11 @@ class AppAutoUpdater:
raise Exception( raise Exception(
f"Too many assets matching regex '{asset}' for release {latest_version} : {matching_assets_urls}. Full release details on {latest_release_html_url}" f"Too many assets matching regex '{asset}' for release {latest_version} : {matching_assets_urls}. Full release details on {latest_release_html_url}"
) )
return latest_version, matching_assets_urls[0] return (
latest_version,
matching_assets_urls[0],
latest_release_html_url,
)
elif isinstance(asset, dict): elif isinstance(asset, dict):
matching_assets_dicts = {} matching_assets_dicts = {}
for asset_name, asset_regex in asset.items(): for asset_name, asset_regex in asset.items():
@ -344,7 +365,11 @@ class AppAutoUpdater:
f"Too many assets matching regex '{asset}' for release {latest_version} : {matching_assets_urls}. Full release details on {latest_release_html_url}" f"Too many assets matching regex '{asset}' for release {latest_version} : {matching_assets_urls}. Full release details on {latest_release_html_url}"
) )
matching_assets_dicts[asset_name] = matching_assets_urls[0] matching_assets_dicts[asset_name] = matching_assets_urls[0]
return latest_version.strip("v"), matching_assets_dicts return (
latest_version.strip("v"),
matching_assets_dicts,
latest_release_html_url,
)
elif strategy == "latest_github_tag" or strategy == "latest_gitlab_tag": elif strategy == "latest_github_tag" or strategy == "latest_gitlab_tag":
if asset != "tarball": if asset != "tarball":
@ -367,8 +392,12 @@ class AppAutoUpdater:
latest_commit = commits[0] latest_commit = commits[0]
latest_tarball = api.url_for_ref(latest_commit["sha"], RefType.commits) latest_tarball = api.url_for_ref(latest_commit["sha"], RefType.commits)
# Let's have the version as something like "2023.01.23" # Let's have the version as something like "2023.01.23"
latest_commit_date = datetime.strptime(latest_commit["commit"]["author"]["date"][:10], "%Y-%m-%d") latest_commit_date = datetime.strptime(
version_format = infos.get("autoupdate", {}).get("force_version", "%Y.%m.%d") latest_commit["commit"]["author"]["date"][:10], "%Y-%m-%d"
)
version_format = infos.get("autoupdate", {}).get(
"force_version", "%Y.%m.%d"
)
latest_version = latest_commit_date.strftime(version_format) latest_version = latest_commit_date.strftime(version_format)
return latest_version, latest_tarball return latest_version, latest_tarball
@ -376,7 +405,6 @@ class AppAutoUpdater:
def replace_version_and_asset_in_manifest( def replace_version_and_asset_in_manifest(
self, content, new_version, new_assets_urls, current_assets, is_main self, content, new_version, new_assets_urls, current_assets, is_main
): ):
if isinstance(new_assets_urls, str): if isinstance(new_assets_urls, str):
sha256 = sha256_of_remote_file(new_assets_urls) sha256 = sha256_of_remote_file(new_assets_urls)
elif isinstance(new_assets_urls, dict): elif isinstance(new_assets_urls, dict):
@ -387,7 +415,7 @@ class AppAutoUpdater:
if is_main: if is_main:
def repl(m): def repl(m):
return m.group(1) + new_version + "~ynh1\"" return m.group(1) + new_version + '~ynh1"'
content = re.sub( content = re.sub(
r"(\s*version\s*=\s*[\"\'])([\d\.]+)(\~ynh\d+[\"\'])", repl, content r"(\s*version\s*=\s*[\"\'])([\d\.]+)(\~ynh\d+[\"\'])", repl, content
@ -412,7 +440,8 @@ def progressbar(it, prefix="", size=60, file=sys.stdout):
name += " " name += " "
x = int(size * j / count) x = int(size * j / count)
file.write( file.write(
"\n%s[%s%s] %i/%i %s\n" % (prefix, "#" * x, "." * (size - x), j, count, name) "\n%s[%s%s] %i/%i %s\n"
% (prefix, "#" * x, "." * (size - x), j, count, name)
) )
file.flush() file.flush()
@ -432,14 +461,15 @@ def paste_on_haste(data):
TIMEOUT = 3 TIMEOUT = 3
try: try:
url = SERVER_URL + "/documents" url = SERVER_URL + "/documents"
response = requests.post(url, data=data.encode('utf-8'), timeout=TIMEOUT) response = requests.post(url, data=data.encode("utf-8"), timeout=TIMEOUT)
response.raise_for_status() response.raise_for_status()
dockey = response.json()['key'] dockey = response.json()["key"]
return SERVER_URL + "/raw/" + dockey return SERVER_URL + "/raw/" + dockey
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
print("\033[31mError: {}\033[0m".format(e)) print("\033[31mError: {}\033[0m".format(e))
sys.exit(1) sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":
args = [arg for arg in sys.argv[1:] if arg != "--commit-and-create-PR"] args = [arg for arg in sys.argv[1:] if arg != "--commit-and-create-PR"]
@ -455,6 +485,7 @@ if __name__ == "__main__":
except Exception as e: except Exception as e:
apps_failed.append(app) apps_failed.append(app)
import traceback import traceback
t = traceback.format_exc() t = traceback.format_exc()
apps_failed_details[app] = t apps_failed_details[app] = t
print(t) print(t)
@ -465,8 +496,15 @@ if __name__ == "__main__":
if apps_failed: if apps_failed:
print(f"Apps failed: {', '.join(apps_failed)}") print(f"Apps failed: {', '.join(apps_failed)}")
if os.path.exists("/usr/bin/sendxmpppy"): if os.path.exists("/usr/bin/sendxmpppy"):
paste = '\n=========\n'.join([app + "\n-------\n" + trace + "\n\n" for app, trace in apps_failed_details.items()]) paste = "\n=========\n".join(
paste_url = paste_on_haste(paste) [
os.system(f"/usr/bin/sendxmpppy 'Failed to run the source auto-update for : {', '.join(apps_failed)}. Please run manually the `autoupdate_app_sources.py` script on these apps to debug what is happening! Debug log : {paste_url}'") app + "\n-------\n" + trace + "\n\n"
for app, trace in apps_failed_details.items()
]
)
paste_url = paste_on_haste(paste)
os.system(
f"/usr/bin/sendxmpppy 'Failed to run the source auto-update for : {', '.join(apps_failed)}. Please run manually the `autoupdate_app_sources.py` script on these apps to debug what is happening! Debug log : {paste_url}'"
)
if apps_updated: if apps_updated:
print(f"Apps updated: {', '.join(apps_updated)}") print(f"Apps updated: {', '.join(apps_updated)}")

View file

@ -37,6 +37,12 @@ def get_wishlist() -> Dict[str, Dict[str, str]]:
return toml.load(wishlist_path) return toml.load(wishlist_path)
@cache
def get_graveyard() -> Dict[str, Dict[str, str]]:
wishlist_path = APPS_ROOT / "graveyard.toml"
return toml.load(wishlist_path)
def validate_schema() -> Generator[str, None, None]: def validate_schema() -> Generator[str, None, None]:
with open(APPS_ROOT / "schemas" / "apps.toml.schema.json", encoding="utf-8") as file: with open(APPS_ROOT / "schemas" / "apps.toml.schema.json", encoding="utf-8") as file:
apps_catalog_schema = json.load(file) apps_catalog_schema = json.load(file)
@ -50,9 +56,6 @@ def check_app(app: str, infos: Dict[str, Any]) -> Generator[Tuple[str, bool], No
yield "state is missing", True yield "state is missing", True
return return
if infos["state"] != "working":
return
# validate that the app is not (anymore?) in the wishlist # validate that the app is not (anymore?) in the wishlist
# we use fuzzy matching because the id in catalog may not be the same exact id as in the wishlist # we use fuzzy matching because the id in catalog may not be the same exact id as in the wishlist
# some entries are ignore-hard-coded, because e.g. radarr an readarr are really different apps... # some entries are ignore-hard-coded, because e.g. radarr an readarr are really different apps...
@ -66,6 +69,16 @@ def check_app(app: str, infos: Dict[str, Any]) -> Generator[Tuple[str, bool], No
if wishlist_matches: if wishlist_matches:
yield f"app seems to be listed in wishlist: {wishlist_matches}", True yield f"app seems to be listed in wishlist: {wishlist_matches}", True
ignored_graveyard_entries = ["mailman"]
graveyard_matches = [
grave
for grave in get_graveyard()
if grave not in ignored_graveyard_entries
and SequenceMatcher(None, app, grave).ratio() > 0.9
]
if graveyard_matches:
yield f"app seems to be listed in graveyard: {graveyard_matches}", True
repo_name = infos.get("url", "").split("/")[-1] repo_name = infos.get("url", "").split("/")[-1]
if repo_name != f"{app}_ynh": if repo_name != f"{app}_ynh":
yield f"repo name should be {app}_ynh, not in {repo_name}", True yield f"repo name should be {app}_ynh, not in {repo_name}", True

View file

@ -20,7 +20,7 @@ website = "https://ajenti.org"
name = "Akaunting" name = "Akaunting"
description = "Manage payments/invoices/expenses" description = "Manage payments/invoices/expenses"
upstream = "https://github.com/akaunting/akaunting" upstream = "https://github.com/akaunting/akaunting"
website = "" website = "https://akaunting.com/"
[amara] [amara]
name = "Amara" name = "Amara"
@ -28,11 +28,17 @@ description = "Collaborative translation of subtitles for videosCollaborative tr
upstream = "https://gitlab.com/hanklank/amara-archive" upstream = "https://gitlab.com/hanklank/amara-archive"
website = "https://amara.org" website = "https://amara.org"
[amusewiki]
name = "Amusewiki"
description = "A library-oriented wiki engine and a powerful authoring, archiving and publishing platform."
upstream = "https://github.com/melmothx/amusewiki"
website = "https://amusewiki.org"
[anki-sync-server] [anki-sync-server]
name = "Anki Sync Server" name = "Anki Sync Server"
description = "a personal Anki server" description = "a personal Anki server"
upstream = "https://github.com/ankicommunity/anki-sync-server" upstream = "https://github.com/ankicommunity/anki-sync-server"
website = "" website = "https://apps.ankiweb.net/"
[anonaddy] [anonaddy]
name = "AnonAddy" name = "AnonAddy"
@ -112,11 +118,18 @@ description = "No-code database tool, alternative to Airtable"
upstream = "https://gitlab.com/bramw/baserow" upstream = "https://gitlab.com/bramw/baserow"
website = "https://baserow.io/" website = "https://baserow.io/"
[bearblog]
name = "Bearblog"
description = "Free, no-nonsense, super-fast blogging"
upstream = "https://github.com/HermanMartinus/bearblog/"
website = "https://bearblog.dev/"
[beatbump] [beatbump]
name = "Beatbump" name = "Beatbump"
description = "An alternative frontend for YouTube Music" description = "An alternative frontend for YouTube Music"
upstream = "https://github.com/snuffyDev/Beatbump" upstream = "https://github.com/snuffyDev/Beatbump"
website = "https://beatbump.ml/home" website = "https://beatbump.ml/home"
draft = "https://github.com/YunoHost-Apps/beatbump_ynh"
[beeper] [beeper]
name = "Beeper" name = "Beeper"
@ -136,6 +149,12 @@ description = "Web conferencing system"
upstream = "https://github.com/bigbluebutton/bigbluebutton" upstream = "https://github.com/bigbluebutton/bigbluebutton"
website = "https://bigbluebutton.org" website = "https://bigbluebutton.org"
[birdsitelive]
name = "BirdsiteLive"
description = "ActivityPub bridge from Twitter"
upstream = "https://github.com/NicolasConstant/BirdsiteLive"
website = ""
[bitcartcc] [bitcartcc]
name = "BitcartCC" name = "BitcartCC"
description = "All-in-one cryptocurrency solution" description = "All-in-one cryptocurrency solution"
@ -152,7 +171,7 @@ website = "https://bitmessage.org/"
name = "Blynk" name = "Blynk"
description = "Blynk library for embedded hardware. Works with Arduino, ESP8266, Raspberry Pi, Intel Edison/Galileo, LinkIt ONE, Particle Core/Photon, Energia, ARM mbed, etc." description = "Blynk library for embedded hardware. Works with Arduino, ESP8266, Raspberry Pi, Intel Edison/Galileo, LinkIt ONE, Particle Core/Photon, Energia, ARM mbed, etc."
upstream = "https://github.com/blynkkk/blynk-library" upstream = "https://github.com/blynkkk/blynk-library"
website = "" website = "https://blynk.io/"
[boinc] [boinc]
name = "BOINC" name = "BOINC"
@ -194,13 +213,13 @@ website = "https://cal.com/"
name = "changedetection.io" name = "changedetection.io"
description = "Monitor changes in web pages" description = "Monitor changes in web pages"
upstream = "https://github.com/dgtlmoon/changedetection.io" upstream = "https://github.com/dgtlmoon/changedetection.io"
website = "" website = "https://changedetection.io/"
[chaskiq] [chaskiq]
name = "Chaskiq" name = "Chaskiq"
description = "A full featured Live Chat, Support & Marketing platform, alternative to Intercom, Drift, Crisp" description = "A full featured Live Chat, Support & Marketing platform, alternative to Intercom, Drift, Crisp"
upstream = "https://github.com/chaskiq/chaskiq" upstream = "https://github.com/chaskiq/chaskiq"
website = "" website = "https://chaskiq.io/"
[chatterbox] [chatterbox]
name = "Chatterbox" name = "Chatterbox"
@ -232,6 +251,12 @@ description = "A tool for making open data websites"
upstream = "https://github.com/ckan/ckan" upstream = "https://github.com/ckan/ckan"
website = "https://ckan.org/" website = "https://ckan.org/"
[claper]
name = "Claper"
description = "Claper turns your presentations into an interactive, engaging and exciting experience."
upstream = "https://github.com/ClaperCo/claper"
website = "https://claper.co/"
[clearflask] [clearflask]
name = "ClearFlask" name = "ClearFlask"
description = "Ideation Tool for Feedback, Roadmap and Announcements" description = "Ideation Tool for Feedback, Roadmap and Announcements"
@ -244,6 +269,12 @@ description = "CloudTube front-end for YouTube"
upstream = "https://git.sr.ht/~cadence/cloudtube" upstream = "https://git.sr.ht/~cadence/cloudtube"
website = "https://tube.cadence.moe/" website = "https://tube.cadence.moe/"
[cobalt]
name = "Cobalt"
description = "Simple Media downloader GUI"
upstream = "https://github.com/wukko/cobalt"
website = "https://cobalt.tools/"
[commafeed] [commafeed]
name = "Commafeed" name = "Commafeed"
description = "RSS reader" description = "RSS reader"
@ -267,6 +298,7 @@ name = "Coquelicot"
description = "A “one-click” file sharing web application" description = "A “one-click” file sharing web application"
upstream = "" upstream = ""
website = "https://coquelicot.potager.org/" website = "https://coquelicot.potager.org/"
draft = "https://github.com/YunoHost-Apps/coquelicot_ynh"
[counter] [counter]
name = "Counter" name = "Counter"
@ -305,11 +337,23 @@ upstream = "https://github.com/mguessan/davmail"
website = "http://davmail.sourceforge.net/" website = "http://davmail.sourceforge.net/"
[digibuzzer] [digibuzzer]
name = "DIGIBUZZER" name = "Digibuzzer"
description = "pour jouer autour d'un buzzer connecté" description = "pour jouer autour d'un buzzer connecté"
upstream = "https://codeberg.org/ladigitale/digibuzzer" upstream = "https://codeberg.org/ladigitale/digibuzzer"
website = "https://digibuzzer.app/" website = "https://digibuzzer.app/"
[digishare]
name = "Digishare"
description = "pour partager des fichiers avec des appareils proches"
upstream = "https://codeberg.org/ladigitale/digishare"
website = "https://ladigitale.dev/digishare/"
[digistorm]
name = "Digistorm"
description = "pour créer des remue-méninges, des questionnaires, etc."
upstream = "https://codeberg.org/ladigitale/digistorm"
website = "https://digistorm.app/"
[directus] [directus]
name = "Directus" name = "Directus"
description = "Real-time API and intuitive no-code data collaboration app for any SQL database" description = "Real-time API and intuitive no-code data collaboration app for any SQL database"
@ -332,7 +376,7 @@ website = ""
name = "Docspell" name = "Docspell"
description = "Simple document organizer" description = "Simple document organizer"
upstream = "https://github.com/eikek/docspell" upstream = "https://github.com/eikek/docspell"
website = "" website = "https://docspell.org/"
[docusaurus] [docusaurus]
name = "Docusaurus" name = "Docusaurus"
@ -340,6 +384,12 @@ description = "Static site generator/SPA to build documentations"
upstream = "https://github.com/facebook/docusaurus" upstream = "https://github.com/facebook/docusaurus"
website = "" website = ""
[docuseal]
name = "DocuSeal"
description = "Create, fill, and sign digital documents"
upstream = "https://github.com/docusealco/docuseal"
website = "https://www.docuseal.co/"
[dokos] [dokos]
name = "Dokos" name = "Dokos"
description = "Plateforme de gestion pour votre Entreprise. Adaptation française d'ERPNext." description = "Plateforme de gestion pour votre Entreprise. Adaptation française d'ERPNext."
@ -400,6 +450,12 @@ description = "Modern intranet, social network, community management platform, c
upstream = "https://github.com/exoplatform/" upstream = "https://github.com/exoplatform/"
website = "https://www.exoplatform.com" website = "https://www.exoplatform.com"
[faircamp]
name = "Faircamp"
description = "Static site generator for audio artists and producers"
upstream = "https://codeberg.org/simonrepp/faircamp"
website = "https://simonrepp.com/faircamp/"
[farside] [farside]
name = "Farside" name = "Farside"
description = "A redirecting service for FOSS alternative frontends" description = "A redirecting service for FOSS alternative frontends"
@ -466,11 +522,11 @@ description = "Online service aggregator hub"
upstream = "https://github.com/mozilla/togetherjs" upstream = "https://github.com/mozilla/togetherjs"
website = "" website = ""
[freescout] [frigate]
name = "Freescout" name = "Frigate"
description = "Helpdesk & Shared Mailbox" description = "Local NVR designed for Home Assistant with AI object detection"
upstream = "https://github.com/freescout-helpdesk/freescout" upstream = "https://github.com/blakeblackshear/frigate"
website = "https://freescout.net/" website = "https://frigate.video/"
[gatsby] [gatsby]
name = "Gatsby" name = "Gatsby"
@ -484,6 +540,18 @@ description = "Genealogy in a web interface"
upstream = "https://github.com/geneweb/geneweb" upstream = "https://github.com/geneweb/geneweb"
website = "https://geneweb.tuxfamily.org" website = "https://geneweb.tuxfamily.org"
[geovisio]
name = "GeoVisio"
description = "Self-hosting geo-located street pictures solution"
upstream = "https://gitlab.com/geovisio"
website = "https://geovisio.fr/"
[gladys-assistant]
name = "Gladys Assistant"
description = "A privacy-first, open-source home assistant."
upstream = "https://github.com/gladysassistant/gladys"
website = "https://gladysassistant.com/"
[goaccess] [goaccess]
name = "Goaccess" name = "Goaccess"
description = "Web log analyzer" description = "Web log analyzer"
@ -507,12 +575,25 @@ name = "Gollum"
description = "A simple Git-powered wiki" description = "A simple Git-powered wiki"
upstream = "https://github.com/gollum/gollum" upstream = "https://github.com/gollum/gollum"
website = "" website = ""
draft = "https://github.com/YunoHost-Apps/gollum_ynh"
[gothub]
name = "GotHub"
description = "An alternative front-end for GitHub, written in Go."
upstream = "https://codeberg.org/gothub/gothub"
website = "https://gothub.app/"
[gramps-web]
name = "Gramps Web"
description = "Collaborative genealogy, based on and interoperable with Gramps."
upstream = "https://github.com/gramps-project/Gramps.js"
website = "https://www.grampsweb.org/"
[granary] [granary]
name = "Granary" name = "Granary"
description = "💬 The social web translator" description = "💬 The social web translator"
upstream = "https://github.com/snarfed/granary" upstream = "https://github.com/snarfed/granary"
website = "" website = "https://granary.io/"
[graphhopper] [graphhopper]
name = "Graphhopper" name = "Graphhopper"
@ -526,12 +607,6 @@ description = "A really simple end-user interface for your BigBlueButton server"
upstream = "https://github.com/bigbluebutton/greenlight" upstream = "https://github.com/bigbluebutton/greenlight"
website = "https://blabla.aquilenet.fr/b" website = "https://blabla.aquilenet.fr/b"
[grist]
name = "Grist"
description = "The evolution of spreadsheets"
upstream = "https://github.com/gristlabs/grist-core/"
website = "https://www.getgrist.com/"
[habitica] [habitica]
name = "Habitica" name = "Habitica"
description = "A habit tracker app which treats your goals like a Role Playing Game." description = "A habit tracker app which treats your goals like a Role Playing Game."
@ -544,11 +619,17 @@ description = "Vehicle expense tracking system"
upstream = "https://github.com/akhilrex/hammond" upstream = "https://github.com/akhilrex/hammond"
website = "" website = ""
[hauk]
name = "Hauk"
description = "Realtime location sharing"
upstream = "https://github.com/bilde2910/Hauk"
website = ""
[helpy] [helpy]
name = "Helpy" name = "Helpy"
description = "A modern helpdesk customer support app, including knowledgebase, discussions and tickets" description = "A modern helpdesk customer support app, including knowledgebase, discussions and tickets"
upstream = "https://github.com/helpyio/helpy" upstream = "https://github.com/helpyio/helpy"
website = "" website = "https://helpy.io/"
[hexo] [hexo]
name = "Hexo" name = "Hexo"
@ -594,10 +675,17 @@ website = "https://v2.hysteria.network/"
[icecast-2] [icecast-2]
name = "Icecast 2" name = "Icecast 2"
description = "" description = "Streaming media server supporting Ogg, Opus, WebM and MP3 streams"
upstream = "https://gitlab.xiph.org/xiph/icecast-server/" upstream = "https://gitlab.xiph.org/xiph/icecast-server/"
website = "https://www.icecast.org" website = "https://www.icecast.org"
[immich]
name = "Immich"
description = "Self-hosted backup solution for photos and videos on mobile device. Alternative to Google Photo."
upstream = "https://github.com/immich-app/immich"
website = "https://immich.app/"
draft = "https://github.com/YunoHost-Apps/immich_ynh"
[infcloud] [infcloud]
name = "InfCloud" name = "InfCloud"
description = "A contacts, calendar and tasks web client for CalDAV and CardDAV" description = "A contacts, calendar and tasks web client for CalDAV and CardDAV"
@ -616,6 +704,12 @@ description = "A collaborative resource mapper powered by open-knowledge, starti
upstream = "https://github.com/inventaire/inventaire" upstream = "https://github.com/inventaire/inventaire"
website = "https://inventaire.io" website = "https://inventaire.io"
[inventree]
name = "InvenTree"
description = "Inventory management system using Django/python with a nice interface."
upstream = "https://github.com/inventree/inventree"
website = "https://inventree.org/"
[invoiceplane] [invoiceplane]
name = "InvoicePlane" name = "InvoicePlane"
description = "Manage invoices, clients and payments." description = "Manage invoices, clients and payments."
@ -627,12 +721,7 @@ name = "IPFS"
description = "Peer-to-peer hypermedia protocol" description = "Peer-to-peer hypermedia protocol"
upstream = "https://github.com/ipfs/ipfs" upstream = "https://github.com/ipfs/ipfs"
website = "https://ipfs.io" website = "https://ipfs.io"
draft = "https://github.com/YunoHost-Apps/ipfs_ynh"
[joplin]
name = "Joplin"
description = "Note taking and to-do application with synchronisation capabilities for Windows, macOS, Linux, Android and iOS."
upstream = "https://github.com/laurent22/joplin"
website = "https://joplin.cozic.net/"
[js-bin] [js-bin]
name = "JS Bin" name = "JS Bin"
@ -646,6 +735,12 @@ description = "Organize karaoke parties"
upstream = "https://github.com/bhj/karaoke-forever" upstream = "https://github.com/bhj/karaoke-forever"
website = "https://www.karaoke-forever.com/" website = "https://www.karaoke-forever.com/"
[khoj]
name = "Khoj"
description = "AI personal assistant accessible from Emacs, Obsidian or your Web browser"
upstream = "https://github.com/khoj-ai/khoj"
website = "https://khoj.dev/"
[kill-the-newsletter] [kill-the-newsletter]
name = "Kill the newsletter" name = "Kill the newsletter"
description = "Convert email newsletters to RSS feeds" description = "Convert email newsletters to RSS feeds"
@ -657,6 +752,7 @@ name = "Kitchenowl"
description = "Grocery list and recipe manager" description = "Grocery list and recipe manager"
upstream = "https://github.com/TomBursch/kitchenowl" upstream = "https://github.com/TomBursch/kitchenowl"
website = "https://kitchenowl.org/" website = "https://kitchenowl.org/"
draft = "https://github.com/YunoHost-Apps/kitchenowl_ynh"
[klaxon] [klaxon]
name = "Klaxon" name = "Klaxon"
@ -676,11 +772,29 @@ description = "Library system"
upstream = "https://git.koha-community.org/Koha-community/Koha" upstream = "https://git.koha-community.org/Koha-community/Koha"
website = "https://koha-community.org/" website = "https://koha-community.org/"
[koreader-sync-server]
name = "Koreader Sync Server"
description = "Synchronization service for Koreader devices"
upstream = "https://github.com/koreader/koreader-sync-server"
website = "https://koreader.rocks/"
[kutt-it]
name = "Kutt.it"
description = "Link shortener"
upstream = "https://github.com/thedevs-network/kutt"
website = "https://kutt.it/"
[l-atelier] [l-atelier]
name = "L'atelier" name = "L'atelier"
description = "A project management tool" description = "A project management tool"
upstream = "https://github.com/jbl2024/latelier" upstream = "https://github.com/jbl2024/latelier"
website = "" website = "https://jbl2024.github.io/latelier-page/"
[lago]
name = "Lago"
description = "Lago is an open source billing API for product-led SaaS."
upstream = "https://github.com/getlago/lago"
website = "https://www.getlago.com/"
[lesspass] [lesspass]
name = "LessPass" name = "LessPass"
@ -694,6 +808,12 @@ description = "Radio Broadcast & Automation Platform"
upstream = "https://github.com/libretime/libretime" upstream = "https://github.com/libretime/libretime"
website = "https://libretime.org/" website = "https://libretime.org/"
[librum]
name = "Librum"
description = "Read and manage your e-books on any device."
upstream = "https://github.com/Librum-Reader/Librum"
website = "https://librumreader.com/"
[lichen] [lichen]
name = "Lichen" name = "Lichen"
description = "Gemtext to HTML translator" description = "Gemtext to HTML translator"
@ -718,6 +838,12 @@ description = "Minimal, fast, and easy bookmark manager"
upstream = "https://github.com/sissbruecker/linkding" upstream = "https://github.com/sissbruecker/linkding"
website = "" website = ""
[linkwarden]
name = "Linkwarden"
description = "Collaborative bookmark manager"
upstream = "https://github.com/linkwarden/linkwarden"
website = "https://linkwarden.app/"
[liquidsoap] [liquidsoap]
name = "LiquidSoap" name = "LiquidSoap"
description = "Audio and video streaming language" description = "Audio and video streaming language"
@ -734,7 +860,7 @@ website = "https://localai.io"
name = "LocomotiveCMS" name = "LocomotiveCMS"
description = "A platform to create, publish and edit sites" description = "A platform to create, publish and edit sites"
upstream = "https://github.com/locomotivecms/engine" upstream = "https://github.com/locomotivecms/engine"
website = "" website = "https://www.locomotivecms.com/"
[logitech-media-server] [logitech-media-server]
name = "Logitech Media Server" name = "Logitech Media Server"
@ -772,12 +898,24 @@ description = "Music scrobble database, alternative to Last.fm"
upstream = "https://github.com/krateng/maloja" upstream = "https://github.com/krateng/maloja"
website = "https://maloja.krateng.ch" website = "https://maloja.krateng.ch"
[maloja]
name = "Maloja"
description = "Simple self-hosted music scrobble database to create personal listening statistics."
upstream = "https://github.com/krateng/maloja"
website = "https://maloja.krateng.ch/"
[mautrix-discord] [mautrix-discord]
name = "Mautrix-Discord" name = "Mautrix-Discord"
description = "Matrix bridge for Discord" description = "Matrix bridge for Discord"
upstream = "https://github.com/mautrix/discord" upstream = "https://github.com/mautrix/discord"
website = "" website = ""
[mayan-edms]
name = "Mayan-EDMS"
description = "Document management system"
upstream = "https://gitlab.com/mayan-edms/mayan-edms"
website = "https://www.mayan-edms.com/"
[mealie] [mealie]
name = "Mealie" name = "Mealie"
description = "Recipe manager and meal planner" description = "Recipe manager and meal planner"
@ -789,12 +927,14 @@ name = "Mediagoblin"
description = "Video streaming platform" description = "Video streaming platform"
upstream = "https://savannah.gnu.org/projects/mediagoblin" upstream = "https://savannah.gnu.org/projects/mediagoblin"
website = "https://mediagoblin.org/" website = "https://mediagoblin.org/"
draft = "https://github.com/YunoHost-Apps/mediagoblin_ynh"
[medusa] [medusa]
name = "Medusa" name = "Medusa"
description = "Automatic TV shows downloader" description = "Automatic TV shows downloader"
upstream = "" upstream = "https://github.com/pymedusa/Medusa"
website = "https://pymedusa.com/" website = "https://pymedusa.com/"
draft = "https://github.com/YunoHost-Apps/medusa_ynh"
[megaglest] [megaglest]
name = "Megaglest" name = "Megaglest"
@ -812,7 +952,7 @@ website = "https://meshery.io/"
name = "microblog.pub" name = "microblog.pub"
description = "A single-user ActivityPub-powered microblog." description = "A single-user ActivityPub-powered microblog."
upstream = "https://github.com/tsileo/microblog.pub" upstream = "https://github.com/tsileo/microblog.pub"
website = "" website = "https://microblog.pub/"
[mindustry] [mindustry]
name = "Mindustry" name = "Mindustry"
@ -826,6 +966,12 @@ description = "Messaging over Gemini"
upstream = "https://git.sr.ht/~lem/misfin" upstream = "https://git.sr.ht/~lem/misfin"
website = "gemini://misfin.org/" website = "gemini://misfin.org/"
[mixpost]
name = "Mixpost"
description = "Self-hosted social media management"
upstream = "https://github.com/inovector/mixpost"
website = "https://mixpost.app/"
[mkdocs] [mkdocs]
name = "MkDocs" name = "MkDocs"
description = "A fast, simple and downright site generator, building project documentation." description = "A fast, simple and downright site generator, building project documentation."
@ -838,12 +984,6 @@ description = "Mail hosting made simple"
upstream = "https://github.com/modoboa/modoboa" upstream = "https://github.com/modoboa/modoboa"
website = "https://modoboa.org" website = "https://modoboa.org"
[motioneye]
name = "MotionEye"
description = "A web frontend for the motion daemon"
upstream = "https://github.com/ccrisan/motioneye"
website = ""
[nebula] [nebula]
name = "Nebula" name = "Nebula"
description = "Scalable overlay networking tool with a focus on performance, simplicity and security." description = "Scalable overlay networking tool with a focus on performance, simplicity and security."
@ -866,7 +1006,7 @@ website = "https://netlifycms.org/"
name = "Netrunner" name = "Netrunner"
description = "A card game in a cyberpunk universe" description = "A card game in a cyberpunk universe"
upstream = "https://github.com/mtgred/netrunner" upstream = "https://github.com/mtgred/netrunner"
website = "" website = "https://www.jinteki.net/"
[newsblur] [newsblur]
name = "NewsBlur" name = "NewsBlur"
@ -902,7 +1042,7 @@ website = "https://demo.officelife.io/"
name = "OhMyForm" name = "OhMyForm"
description = "Alternative to TypeForm, TellForm, or Google Forms" description = "Alternative to TypeForm, TellForm, or Google Forms"
upstream = "https://github.com/ohmyform/ohmyform" upstream = "https://github.com/ohmyform/ohmyform"
website = "" website = "https://ohmyform.com/"
[omnivore] [omnivore]
name = "Omnivore" name = "Omnivore"
@ -922,12 +1062,25 @@ description = "Shopping cart system. An online e-commerce solution."
upstream = "https://github.com/opencart/opencart" upstream = "https://github.com/opencart/opencart"
website = "https://www.opencart.com" website = "https://www.opencart.com"
[opencast]
draft = "https://github.com/YunoHost-Apps/opencast_ynh"
name = "opencast"
description = "Flexible, reliable, and scalable open source video management system for academic institution"
upstream = "https://github.com/opencast/opencast/"
website = "https://opencast.org/"
[openhab] [openhab]
name = "openHAB" name = "openHAB"
description = "Smart home platform" description = "Smart home platform"
upstream = "https://github.com/openhab/openhab-webui" upstream = "https://github.com/openhab/openhab-webui"
website = "https://www.openhab.org/" website = "https://www.openhab.org/"
[opensign]
name = "OpenSign"
description = "DocuSign alternative, for signing and annotating PDF files"
upstream = "https://github.com/OpenSignLabs/OpenSign"
website = "https://www.opensignlabs.com/"
[organizr] [organizr]
name = "organizr" name = "organizr"
description = "Organizr allows you to setup \"Tabs\" that will be loaded all in one webpage" description = "Organizr allows you to setup \"Tabs\" that will be loaded all in one webpage"
@ -938,7 +1091,7 @@ website = "https://docs.organizr.app/"
name = "OSRM" name = "OSRM"
description = "Routing Machine - C++ backend" description = "Routing Machine - C++ backend"
upstream = "https://github.com/Project-OSRM/osrm-backend" upstream = "https://github.com/Project-OSRM/osrm-backend"
website = "" website = "https://project-osrm.org/"
[otobo] [otobo]
name = "Otobo" name = "Otobo"
@ -970,6 +1123,12 @@ description = "Password manager"
upstream = "https://github.com/passbolt/passbolt_docker" upstream = "https://github.com/passbolt/passbolt_docker"
website = "https://www.passbolt.com" website = "https://www.passbolt.com"
[peer-calls]
name = "peer-calls"
description = "WebRTC group peer to peer video calls for everyone"
upstream = "https://github.com/peer-calls/peer-calls"
website = "https://peercalls.com/"
[penpot] [penpot]
name = "Penpot" name = "Penpot"
description = "Design Freedom for Teams" description = "Design Freedom for Teams"
@ -981,12 +1140,13 @@ name = "Peppermint"
description = "A central hub for your help desk. A powerfully easy system for tracking, prioritising, and solving customer support tickets" description = "A central hub for your help desk. A powerfully easy system for tracking, prioritising, and solving customer support tickets"
upstream = "https://github.com/Peppermint-Lab/peppermint" upstream = "https://github.com/Peppermint-Lab/peppermint"
website = "https://peppermint.sh/" website = "https://peppermint.sh/"
draft = "https://github.com/YunoHost-Apps/peppermint_ynh"
[personal-management-system] [personal-management-system]
name = "personal-management-system" name = "personal-management-system"
description = "Your web application for managing personal data." description = "Your web application for managing personal data."
upstream = "https://github.com/Volmarg/personal-management-system" upstream = "https://github.com/Volmarg/personal-management-system"
website = "" website = "http://personal-management-system.pl/"
[phorge] [phorge]
name = "Phorge" name = "Phorge"
@ -1005,6 +1165,7 @@ name = "PIA"
description = "A tool to help carrying out Privacy Impact Assessments" description = "A tool to help carrying out Privacy Impact Assessments"
upstream = "https://github.com/LINCnil/pia" upstream = "https://github.com/LINCnil/pia"
website = "" website = ""
draft = "https://github.com/YunoHost-Apps/pia_ynh"
[picsur] [picsur]
name = "Picsur" name = "Picsur"
@ -1030,18 +1191,18 @@ description = "Project planning tool"
upstream = "https://github.com/makeplane/plane" upstream = "https://github.com/makeplane/plane"
website = "https://plane.so/" website = "https://plane.so/"
[planka]
name = "Planka"
description = "Kanban board for workgroups."
upstream = "https://github.com/plankanban/planka"
website = "https://planka.app/"
[plausible-analytics] [plausible-analytics]
name = "Plausible Analytics" name = "Plausible Analytics"
description = "Privacy-friendly web analytics (alternative to Google Analytics)" description = "Privacy-friendly web analytics (alternative to Google Analytics)"
upstream = "https://github.com/plausible/analytics" upstream = "https://github.com/plausible/analytics"
website = "https://plausible.io" website = "https://plausible.io"
[polis]
name = "Polis"
description = "Gathers and analyzes what large groups of people think in their own words. Creates shared decisions."
upstream = "https://github.com/compdemocracy/polis"
website = "https://pol.is"
[pretix] [pretix]
name = "pretix" name = "pretix"
description = "All-in-one ticketing software" description = "All-in-one ticketing software"
@ -1071,6 +1232,7 @@ name = "Proxigram"
description = "Front-end for Instagram, providing also RSS" description = "Front-end for Instagram, providing also RSS"
upstream = "https://codeberg.org/ThePenguinDev/Proxigram" upstream = "https://codeberg.org/ThePenguinDev/Proxigram"
website = "" website = ""
draft = "https://github.com/YunoHost-Apps/proxigram_ynh"
[psono] [psono]
name = "Psono" name = "Psono"
@ -1080,9 +1242,10 @@ website = "https://psono.com/"
[pterodactyl] [pterodactyl]
name = "Pterodactyl" name = "Pterodactyl"
description = "" description = "Game server management panel"
upstream = "" upstream = "https://github.com/pterodactyl/panel"
website = "https://pterodactyl.io/" website = "https://pterodactyl.io/"
draft = "https://github.com/YunoHost-Apps/pterodactyl_ynh"
[qgis-server] [qgis-server]
name = "QGis server" name = "QGis server"
@ -1130,7 +1293,7 @@ website = "https://app.rawgraphs.io/"
name = "Redash" name = "Redash"
description = "Connect to any data source, easily visualize, dashboard and share your data." description = "Connect to any data source, easily visualize, dashboard and share your data."
upstream = "https://github.com/getredash/redash" upstream = "https://github.com/getredash/redash"
website = "" website = "https://redash.io/"
[renovate] [renovate]
name = "Renovate" name = "Renovate"
@ -1166,7 +1329,7 @@ website = "https://revolt.chat/"
name = "RSS-proxy" name = "RSS-proxy"
description = "Create an RSS or ATOM feed of almost any website, just by analyzing just the static HTML structure." description = "Create an RSS or ATOM feed of almost any website, just by analyzing just the static HTML structure."
upstream = "https://github.com/damoeb/rss-proxy" upstream = "https://github.com/damoeb/rss-proxy"
website = "" website = "https://rssproxy.migor.org/"
[sabnzbd] [sabnzbd]
name = "SABnzbd" name = "SABnzbd"
@ -1179,6 +1342,7 @@ name = "SAT"
description = "An all-in-one tool to manage all your communications" description = "An all-in-one tool to manage all your communications"
upstream = "" upstream = ""
website = "https://salut-a-toi.org" website = "https://salut-a-toi.org"
draft = "https://github.com/YunoHost-Apps/sat_ynh"
[screego] [screego]
name = "Screego" name = "Screego"
@ -1191,6 +1355,7 @@ name = "Scribe"
description = "An alternative frontend to Medium" description = "An alternative frontend to Medium"
upstream = "https://git.sr.ht/~edwardloveall/scribe" upstream = "https://git.sr.ht/~edwardloveall/scribe"
website = "https://scribe.rip/" website = "https://scribe.rip/"
draft = "https://github.com/YunoHost-Apps/scribe_ynh"
[semantic-mediawiki] [semantic-mediawiki]
name = "Semantic MediaWiki" name = "Semantic MediaWiki"
@ -1202,24 +1367,26 @@ website = "https://www.semantic-mediawiki.org/wiki/Semantic_MediaWiki"
name = "Semaphore" name = "Semaphore"
description = "A fediverse (Mastodon-API compatible) accessible, simple and fast web client" description = "A fediverse (Mastodon-API compatible) accessible, simple and fast web client"
upstream = "https://github.com/NickColley/semaphore" upstream = "https://github.com/NickColley/semaphore"
website = "" website = "https://semaphore.social/"
[shadowsocks] [shadowsocks]
name = "shadowsocks" name = "shadowsocks"
description = "A SOCKS5 proxy to protect your Internet traffic" description = "A SOCKS5 proxy to protect your Internet traffic"
upstream = "" upstream = "https://github.com/shadowsocks/shadowsocks-org"
website = "https://shadowsocks.org" website = "https://shadowsocks.org"
draft = "https://github.com/YunoHost-Apps/shadowsocks_ynh"
[shinken] [shinken]
name = "shinken" name = "shinken"
description = "A flexible and scalable monitoring framework" description = "A flexible and scalable monitoring framework"
upstream = "https://github.com/naparuba/shinken" upstream = "https://github.com/naparuba/shinken"
website = "" website = "http://www.shinken-monitoring.org/"
draft = "https://github.com/YunoHost-Apps/shinken_ynh"
[sickrage] [sickchill]
name = "sickrage" name = "sickchill"
description = "Automatic TV shows downloader" description = "Automatic TV shows downloader"
upstream = "" upstream = "https://github.com/SickChill/sickchill"
website = "https://sickchill.github.io/" website = "https://sickchill.github.io/"
[signal-proxy] [signal-proxy]
@ -1228,6 +1395,12 @@ description = "Fight censorship and bypass traffic securely to the Signal servic
upstream = "https://github.com/signalapp/Signal-TLS-Proxy" upstream = "https://github.com/signalapp/Signal-TLS-Proxy"
website = "https://signal.org/blog/help-iran-reconnect/" website = "https://signal.org/blog/help-iran-reconnect/"
[silverbullet]
name = "SilverBullet"
description = "Extensible personal knowledge management system with plain markdown files."
upstream = "https://github.com/silverbulletmd/silverbullet"
website = "https://silverbullet.md/"
[simplelogin] [simplelogin]
name = "SimpleLogin" name = "SimpleLogin"
description = "Privacy-first e-mail forwarding and identity provider service" description = "Privacy-first e-mail forwarding and identity provider service"
@ -1250,7 +1423,8 @@ website = "https://socialhome.network"
name = "sphinx" name = "sphinx"
description = "The Sphinx documentation generator" description = "The Sphinx documentation generator"
upstream = "https://github.com/sphinx-doc/sphinx" upstream = "https://github.com/sphinx-doc/sphinx"
website = "" website = "https://www.sphinx-doc.org/"
draft = "https://github.com/YunoHost-Apps/sphinx_ynh"
[spodcast] [spodcast]
name = "Spodcast" name = "Spodcast"
@ -1276,6 +1450,12 @@ description = "Gallery/Camera application with private encrypted Backup and Sync
upstream = "https://github.com/stingle/stingle-api" upstream = "https://github.com/stingle/stingle-api"
website = "https://stingle.org/" website = "https://stingle.org/"
[stirling-pdf]
name = "Stirling PDF"
description = "Edit, compress, sign, OCR and other various operations on PDF files"
upstream = "https://github.com/Frooodle/Stirling-PDF"
website = ""
[storj] [storj]
name = "Storj" name = "Storj"
description = "Ongoing Storj v3 development. Decentralized cloud object storage that is affordable, easy to use, private, and secure." description = "Ongoing Storj v3 development. Decentralized cloud object storage that is affordable, easy to use, private, and secure."
@ -1304,13 +1484,14 @@ website = "https://suitecrm.com/"
name = "Superalgos" name = "Superalgos"
description = "Crypto trading bot, automated bitcoin / cryptocurrency trading software." description = "Crypto trading bot, automated bitcoin / cryptocurrency trading software."
upstream = "https://github.com/Superalgos/Superalgos" upstream = "https://github.com/Superalgos/Superalgos"
website = "" website = "https://superalgos.org/"
[sympa] [sympa]
name = "Sympa" name = "Sympa"
description = "Mailing List manager" description = "Mailing List manager"
upstream = "" upstream = "https://github.com/sympa-community/sympa"
website = "https://www.sympa.org/" website = "https://www.sympa.community/"
draft = "https://github.com/YunoHost-Apps/sympa_ynh"
[syspass] [syspass]
name = "Syspass" name = "Syspass"
@ -1326,7 +1507,7 @@ website = "https://tahoe-lafs.org/"
[taiga] [taiga]
name = "Taiga" name = "Taiga"
description = "" description = "Project management"
upstream = "https://github.com/kaleidos-ventures/taiga-back" upstream = "https://github.com/kaleidos-ventures/taiga-back"
website = "https://taiga.io" website = "https://taiga.io"
@ -1360,6 +1541,12 @@ description = "Multi-protocol access proxy which understands SSH, HTTPS, RDP, Ku
upstream = "https://github.com/gravitational/teleport" upstream = "https://github.com/gravitational/teleport"
website = "https://goteleport.com/" website = "https://goteleport.com/"
[teslamate]
name = "Teslamate"
description = "A powerful, self-hosted data logger for your Tesla"
upstream = "https://github.com/adriankumpf/teslamate"
website = "https://docs.teslamate.org/docs/installation/docker"
[theia-ide] [theia-ide]
name = "Theia-IDE" name = "Theia-IDE"
description = "VS Code-like cloud IDE" description = "VS Code-like cloud IDE"
@ -1384,12 +1571,6 @@ description = "Instant Terminal Sharing"
upstream = "https://github.com/tmate-io/tmate" upstream = "https://github.com/tmate-io/tmate"
website = "https://tmate.io/" website = "https://tmate.io/"
[traccar]
name = "Traccar"
description = "Modern GPS Tracking Platform"
upstream = "https://github.com/traccar/traccar"
website = ""
[trivy] [trivy]
name = "trivy" name = "trivy"
description = "OSS Vulnerability and Misconfiguration Scanning." description = "OSS Vulnerability and Misconfiguration Scanning."
@ -1405,14 +1586,15 @@ website = "https://www.tryton.org/"
[tubesync] [tubesync]
name = "tubesync" name = "tubesync"
description = "Syncs YouTube channels and playlists to a locally hosted media server" description = "Syncs YouTube channels and playlists to a locally hosted media server"
upstream = "https://github.com/meeb/tubesyn" upstream = "https://github.com/meeb/tubesync"
website = "" website = ""
[tutao] [tutao]
name = "tutao" name = "tutao"
description = "End-to-end encrypted e-mail client" description = "End-to-end encrypted e-mail client"
upstream = "https://github.com/tutao/tutanota/" upstream = "https://github.com/tutao/tutanota/"
website = "" website = "https://tuta.com/"
draft = "https://github.com/YunoHost-Apps/tutao_ynh"
[twake-app] [twake-app]
name = "Twake.app" name = "Twake.app"
@ -1431,6 +1613,7 @@ name = "umap"
description = "Cartography software" description = "Cartography software"
upstream = "" upstream = ""
website = "https://umap.openstreetmap.fr/" website = "https://umap.openstreetmap.fr/"
draft = "https://github.com/YunoHost-Apps/umap_ynh"
[upmpdcli] [upmpdcli]
name = "upmpdcli" name = "upmpdcli"
@ -1450,6 +1633,18 @@ description = "Build and share document collections"
upstream = "https://github.com/huridocs/uwazi" upstream = "https://github.com/huridocs/uwazi"
website = "https://www.uwazi.io/" website = "https://www.uwazi.io/"
[vod2podrss]
name = "Vod2PodRSS"
description = "Convert YouTube or Twitch channels RSS feed"
upstream = "https://github.com/madiele/vod2pod-rss"
website = ""
[voyantserver]
name = "VoyantServer"
description = "Runs a webUI and backend for VoyantTools, a textual concordance and analysis java app."
upstream = "https://github.com/voyanttools/VoyantServer"
website = "https://voyant-tools.org/"
[vpn-server] [vpn-server]
name = "VPN server" name = "VPN server"
description = "Create/provide VPNs from your server" description = "Create/provide VPNs from your server"
@ -1467,12 +1662,13 @@ name = "webogram"
description = "A new era of messaging" description = "A new era of messaging"
upstream = "https://github.com/zhukov/webogram" upstream = "https://github.com/zhukov/webogram"
website = "" website = ""
draft = "https://github.com/YunoHost-Apps/webogram_ynh"
[webterminal] [webterminal]
name = "Webterminal" name = "Webterminal"
description = "A web-based Jump Host / Bastion, supports VNC, SSH, RDP, Telnet, SFTP..." description = "A web-based Jump Host / Bastion, supports VNC, SSH, RDP, Telnet, SFTP..."
upstream = "https://github.com/jimmy201602/webterminal/" upstream = "https://github.com/jimmy201602/webterminal/"
website = "" website = "https://jimmy201602.github.io/webterminal/"
[webthings-gateway] [webthings-gateway]
name = "WebThings Gateway" name = "WebThings Gateway"
@ -1508,19 +1704,26 @@ website = "https://wikisuite.org/Software"
name = "WildDuck" name = "WildDuck"
description = "Opinionated email server" description = "Opinionated email server"
upstream = "https://github.com/nodemailer/wildduck" upstream = "https://github.com/nodemailer/wildduck"
website = "" website = "https://wildduck.email/"
[windmill]
name = "Windmill"
description = "Developer platform for APIs, background jobs, workflows and UIs"
upstream = "https://github.com/windmill-labs/windmill"
website = "https://www.windmill.dev/"
[wisemapping] [wisemapping]
name = "Wisemapping" name = "Wisemapping"
description = "An online mind mapping editor" description = "An online mind mapping editor"
upstream = "https://bitbucket.org/wisemapping/wisemapping-open-source" upstream = "https://bitbucket.org/wisemapping/wisemapping-open-source"
website = "" website = "https://www.wisemapping.com/"
draft = "https://github.com/YunoHost-Apps/wisemapping_ynh"
[workadventure] [workadventure]
name = "WorkAdventure" name = "WorkAdventure"
description = "A web-based collaborative workspace for small to medium teams" description = "A web-based collaborative workspace for small to medium teams"
upstream = "https://github.com/thecodingmachine/workadventure" upstream = "https://github.com/thecodingmachine/workadventure"
website = "" website = "https://workadventu.re/"
[xbrowsersync] [xbrowsersync]
name = "xBrowserSync" name = "xBrowserSync"
@ -1532,11 +1735,11 @@ website = "https://www.xbrowsersync.org/"
name = "Xibo" name = "Xibo"
description = "A FLOSS digital signage solution" description = "A FLOSS digital signage solution"
upstream = "https://github.com/xibosignage/xibo-cms" upstream = "https://github.com/xibosignage/xibo-cms"
website = "" website = "https://xibosignage.com/cms"
[xonotic] [xonotic]
name = "Xonotic" name = "Xonotic"
description = "" description = "Fast paced first person shooter"
upstream = "https://gitlab.com/xonotic" upstream = "https://gitlab.com/xonotic"
website = "https://xonotic.org" website = "https://xonotic.org"
@ -1557,6 +1760,7 @@ name = "Zammad"
description = "Helpdesk/customer support system" description = "Helpdesk/customer support system"
upstream = "https://github.com/zammad/zammad" upstream = "https://github.com/zammad/zammad"
website = "https://zammad.org" website = "https://zammad.org"
draft = "https://github.com/YunoHost-Apps/zammad_ynh"
[zigbee2mqtt-io] [zigbee2mqtt-io]
name = "zigbee2mqtt.io" name = "zigbee2mqtt.io"
@ -1574,10 +1778,17 @@ website = "https://wiki.znc.in/ZNC"
name = "Zoneminder" name = "Zoneminder"
description = "Closed-circuit television software app supporting IP, USB and Analog cameras. " description = "Closed-circuit television software app supporting IP, USB and Analog cameras. "
upstream = "https://github.com/ZoneMinder/zoneminder" upstream = "https://github.com/ZoneMinder/zoneminder"
website = "" website = "https://zoneminder.com/"
[zotero]
name = "Zotero"
description = "collect, organize, annotate, cite, and share research"
upstream = "https://github.com/foxsen/zotero-selfhost"
website = "https://www.zotero.org/"
[zulip] [zulip]
name = "Zulip" name = "Zulip"
description = "Team chat that helps teams stay productive and focused." description = "Team chat that helps teams stay productive and focused."
upstream = "https://github.com/zulip/zulip" upstream = "https://github.com/zulip/zulip"
website = "https://zulipchat.com/" website = "https://zulipchat.com/"
draft = "https://github.com/YunoHost-Apps/zulip_ynh"