Compare commits

..

18 Commits

Author SHA1 Message Date
cedric cac88c39b2 Merge pull request 'feat: add social network field to artiste' (#3) from feat/improve-artist-page into master
Déploiement API BETA / build (push) Successful in 2m4s
Déploiement API PROD / build (push) Successful in 2m5s
Déploiement API BETA / deploy (push) Successful in 45s
Déploiement API PROD / deploy (push) Successful in 51s
Reviewed-on: #3
2026-06-26 17:36:33 +00:00
cedric fcd1e737ab chore: regenerate types after rezoSosyal field
Déploiement API BETA / build (push) Successful in 2m9s
Déploiement API BETA / deploy (push) Successful in 45s
Vérification PR / build (pull_request) Successful in 2m5s
Vérification PR / deploy-beta (pull_request) Successful in 46s
2026-06-26 12:23:41 +04:00
cedric a10d854ee8 feat: add social network component and artiste field 2026-06-26 12:23:29 +04:00
cedric cf2f4de06c Merge pull request 'feat: rendre le dépôt configurable via variables d'env' (#2) from feat/improve-custom into master
Déploiement API BETA / build (push) Successful in 2m13s
Déploiement API PROD / build (push) Successful in 2m6s
Déploiement API BETA / deploy (push) Successful in 45s
Déploiement API PROD / deploy (push) Successful in 55s
Reviewed-on: #2
2026-06-26 04:59:05 +00:00
cedric 36da183404 ci: deploy beta on all branches including master
Déploiement API BETA / build (push) Successful in 2m6s
Vérification PR / build (pull_request) Successful in 2m11s
Déploiement API BETA / deploy (push) Successful in 45s
Vérification PR / deploy-beta (pull_request) Successful in 44s
2026-06-26 07:26:04 +04:00
cedric 58fd049d03 ci: checkout correct branch before deploy
Déploiement API BETA / build (push) Successful in 2m13s
Vérification PR / build (pull_request) Successful in 2m6s
Déploiement API BETA / deploy (push) Successful in 46s
Vérification PR / deploy-beta (pull_request) Successful in 44s
2026-06-26 07:14:13 +04:00
cedric 0a0772eea3 ci: deploy beta on any branch except master 2026-06-26 07:12:55 +04:00
cedric 7fea170597 chore: regenerate types after isExclusiveArtist rename
Vérification PR / build (pull_request) Successful in 2m7s
Vérification PR / deploy-beta (pull_request) Successful in 1m5s
2026-06-26 00:36:34 +04:00
cedric 029ba3cc90 refactor: rename isOKIAwtis to isExclusiveArtist 2026-06-26 00:33:45 +04:00
cedric c8dd6e9c4a feat: use WEBSITE_URL in publication notification emails 2026-06-26 00:33:42 +04:00
cedric afb38a067b feat: use STRAPI_ADMIN_SITE_NAME for admin panel titles 2026-06-26 00:33:32 +04:00
cedric 2540c04782 feat: add titrePhare
Déploiement API PROD / build (push) Successful in 2m8s
Déploiement API PROD / deploy (push) Successful in 53s
2026-06-20 18:37:23 +04:00
cedric 03e2449be3 feat: add isOKIAwtis
Déploiement API PROD / build (push) Successful in 2m11s
Déploiement API PROD / deploy (push) Successful in 56s
2026-06-20 06:08:42 +04:00
cedric ad034a9a6f feat: add karaokeDesktopUrl
Déploiement API PROD / build (push) Successful in 2m4s
Déploiement API PROD / deploy (push) Successful in 56s
2026-06-18 00:33:06 +04:00
cedric 6db371d513 feat: add karaokeUrl
Déploiement API PROD / build (push) Successful in 2m7s
Déploiement API PROD / deploy (push) Successful in 49s
2026-06-17 23:46:56 +04:00
cedric ea0f56c202 feat: add IsNewRelease
Déploiement API PROD / build (push) Successful in 2m10s
Déploiement API PROD / deploy (push) Successful in 50s
2026-06-17 08:49:14 +04:00
cedric 95156de4ca feat: add bulkTranslate to export
Déploiement API PROD / build (push) Successful in 2m9s
Déploiement API PROD / deploy (push) Successful in 51s
2026-06-15 20:19:53 +04:00
cedric 20e701b754 feat: add bulkTranslate 2026-06-15 20:19:30 +04:00
13 changed files with 249 additions and 13 deletions
+3
View File
@@ -6,6 +6,9 @@ PORT=1337
STRAPI_URL= STRAPI_URL=
STRAPI_ADMIN_URL=/admin STRAPI_ADMIN_URL=/admin
# Branding (affiché dans l'interface admin Strapi — rebuild requis)
STRAPI_ADMIN_SITE_NAME=OKI
APP_KEYS= APP_KEYS=
API_TOKEN_SALT= API_TOKEN_SALT=
ADMIN_JWT_SECRET= ADMIN_JWT_SECRET=
+2 -3
View File
@@ -2,8 +2,6 @@ name: Déploiement API BETA
run-name: ${{ gitea.actor }} déploie API BETA run-name: ${{ gitea.actor }} déploie API BETA
on: on:
push: push:
branches:
- dev
jobs: jobs:
build: build:
@@ -39,7 +37,8 @@ jobs:
export NVM_DIR="$HOME/.nvm" export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
cd ${{ secrets.DEPLOY_PATH }} cd ${{ secrets.DEPLOY_PATH }}
git pull --ff-only origin dev git fetch origin
git checkout -B ${{ gitea.ref_name }} origin/${{ gitea.ref_name }}
corepack enable corepack enable
yarn install --frozen-lockfile yarn install --frozen-lockfile
NODE_ENV=production yarn build NODE_ENV=production yarn build
+42
View File
@@ -56,6 +56,26 @@ Si la variable JWT_SECRET n'est pas renseignée, elle est générée automatique
yarn && yarn build && yarn dev yarn && yarn build && yarn dev
``` ```
## Tokens API
Les endpoints marqués ⚙️ **Token requis** nécessitent un token API Strapi.
**Créer le token** : Administration Strapi → *Settings → API Tokens → Create new API Token*
| Endpoint | Type de token | Permissions requises |
|----------|---------------|----------------------|
| `GET /paroles/export` | Custom | Parole → `export` |
| `POST /paroles/bulk-translate` | Custom | Parole → `bulkTranslate` |
Pour un token couvrant les deux endpoints, créer un token de type **Custom** et cocher dans la section *Parole* : `export` et `bulkTranslate`.
Le token est à passer dans le header HTTP :
```
Authorization: Bearer <token>
```
---
## Point d'accès ## Point d'accès
### `/awtis` ### `/awtis`
@@ -77,6 +97,28 @@ ___
### `/paroles/count` ### `/paroles/count`
- `GET` : Récupère le nombre de texte - `GET` : Récupère le nombre de texte
### `/paroles/bulk-translate` ⚙️ Token requis
- `POST` : Traduit automatiquement via DeepL toutes les paroles ayant une source française (`traductions.francais` ou `langueSource: fr`) vers les langues manquantes (EN, ES, DE, IT). Ne modifie pas les traductions déjà existantes.
**Réponse :**
```json
{"translated": 42, "skipped": 18, "errors": []}
```
| Champ | Description |
|-------|-------------|
| `translated` | Nombre de traductions ajoutées |
| `skipped` | Paroles ignorées (pas de source FR ou déjà complètes) |
| `errors` | Erreurs DeepL avec `documentId`, `titre` et `lang` |
**Exemple :**
```bash
curl -X POST -H "Authorization: Bearer <token>" \
"https://api.pawol.nu/api/paroles/bulk-translate"
```
___
### `/paroles/export` ⚙️ Token requis ### `/paroles/export` ⚙️ Token requis
- `GET` : Exporter les paroles et traductions au format JSONL ou JSON pour l'entraînement de modèles LLM - `GET` : Exporter les paroles et traductions au format JSONL ou JSON pour l'entraînement de modèles LLM
+4 -4
View File
@@ -13,14 +13,14 @@ export default {
locales: ['fr'], locales: ['fr'],
translations: { translations: {
fr: { fr: {
'Auth.form.welcome.subtitle': 'Connectez-vous à votre compte OKI API', 'Auth.form.welcome.subtitle': `Connectez-vous à votre compte ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API`,
'Auth.form.welcome.title': 'Bienvenue sur OKI API !', 'Auth.form.welcome.title': `Bienvenue sur ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API !`,
'LeftMenu.navbrand.title': 'Tableau de bord', 'LeftMenu.navbrand.title': 'Tableau de bord',
'LeftMenu.navbrand.workplace': 'Menu', 'LeftMenu.navbrand.workplace': 'Menu',
}, },
en: { en: {
'Auth.form.welcome.subtitle': 'Log in to your OKI API account', 'Auth.form.welcome.subtitle': `Log in to your ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API account`,
'Auth.form.welcome.title': 'Welcome to OKI API !', 'Auth.form.welcome.title': `Welcome to ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API !`,
'LeftMenu.navbrand.title': 'Dashboard', 'LeftMenu.navbrand.title': 'Dashboard',
'LeftMenu.navbrand.workplace': 'Workplace', 'LeftMenu.navbrand.workplace': 'Workplace',
} }
@@ -57,6 +57,20 @@
}, },
"musicBrainzUrl": { "musicBrainzUrl": {
"type": "string" "type": "string"
},
"isExclusiveArtist": {
"type": "boolean",
"default": false
},
"titrePhare": {
"type": "relation",
"relation": "manyToOne",
"target": "api::parole.parole"
},
"rezoSosyal": {
"type": "component",
"repeatable": true,
"component": "social.rezo-sosyal"
} }
} }
} }
@@ -144,6 +144,13 @@ module.exports = {
let {data} = event.params let {data} = event.params
const {documentId} = data const {documentId} = data
if (data.isNewRelease === true) {
await strapi.db.query('api::parole.parole').updateMany({
where: { isNewRelease: true },
data: { isNewRelease: false },
})
}
const previousParoles = await strapi.db.query('api::parole.parole').findOne({ const previousParoles = await strapi.db.query('api::parole.parole').findOne({
where: {documentId}, where: {documentId},
populate: {difference: true, artistes: true} populate: {difference: true, artistes: true}
@@ -186,7 +193,7 @@ module.exports = {
strapi.plugins['email'].services.email.send({ strapi.plugins['email'].services.email.send({
from: process.env.SMTP_FROM, from: process.env.SMTP_FROM,
to: previousData.user.email, to: previousData.user.email,
subject: `Publication de "${previousData.titre}" sur pawol.nu`, subject: `Publication de "${previousData.titre}" sur ${(process.env.WEBSITE_URL || 'https://pawol.nu').replace(/^https?:\/\//, '')}`,
text: `Le titre que vous avez soumis, "${previousData.titre}" a été publié sur le site. text: `Le titre que vous avez soumis, "${previousData.titre}" a été publié sur le site.
Vous pouvez le trouver à l'adresse ${process.env.WEBSITE_URL}/paroles/${previousData.slug} Vous pouvez le trouver à l'adresse ${process.env.WEBSITE_URL}/paroles/${previousData.slug}
Merci pour votre contribution ❤️`, Merci pour votre contribution ❤️`,
@@ -199,7 +206,7 @@ module.exports = {
strapi.plugins['email'].services.email.send({ strapi.plugins['email'].services.email.send({
from: process.env.SMTP_FROM, from: process.env.SMTP_FROM,
to: previousData.userAdmin.email, to: previousData.userAdmin.email,
subject: `Publication de "${previousData.titre}" sur pawol.nu`, subject: `Publication de "${previousData.titre}" sur ${(process.env.WEBSITE_URL || 'https://pawol.nu').replace(/^https?:\/\//, '')}`,
text: `Le titre que vous avez soumis, "${previousData.titre}" a été publié sur le site. text: `Le titre que vous avez soumis, "${previousData.titre}" a été publié sur le site.
Vous pouvez le trouver à l'adresse ${process.env.WEBSITE_URL}/paroles/${previousData.slug}. Vous pouvez le trouver à l'adresse ${process.env.WEBSITE_URL}/paroles/${previousData.slug}.
Merci pour votre contribution ❤️`, Merci pour votre contribution ❤️`,
@@ -142,6 +142,16 @@
"relation": "oneToMany", "relation": "oneToMany",
"target": "api::parole.parole", "target": "api::parole.parole",
"mappedBy": "sourceOriginale" "mappedBy": "sourceOriginale"
},
"isNewRelease": {
"type": "boolean",
"default": false
},
"karaokeUrl": {
"type": "string"
},
"karaokeDesktopUrl": {
"type": "string"
} }
} }
} }
+5
View File
@@ -39,6 +39,11 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({
ctx.body = lines.join('\n') ctx.body = lines.join('\n')
}, },
async bulkTranslate(ctx) {
const result = await strapi.service('api::parole.parole').bulkTranslateMissing()
return ctx.send(result)
},
async findOne(documentId) { async findOne(documentId) {
const parole = await strapi.documents('api::parole.parole').findOne({ const parole = await strapi.documents('api::parole.parole').findOne({
documentId, documentId,
+6 -3
View File
@@ -6,10 +6,13 @@ module.exports = {
method: 'GET', method: 'GET',
path: '/paroles/export', path: '/paroles/export',
handler: 'parole.export', handler: 'parole.export',
config: { config: { policies: [], middlewares: [] },
policies: [],
middlewares: [],
}, },
{
method: 'POST',
path: '/paroles/bulk-translate',
handler: 'parole.bulkTranslate',
config: { policies: [], middlewares: [] },
}, },
], ],
}; };
+73
View File
@@ -42,6 +42,8 @@ function suspectFrench(text) {
return frCount / words.length > 0.04 return frCount / words.length > 0.04
} }
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
class Translator { class Translator {
constructor() { constructor() {
this.deeplApi = process.env.DEEPL_URL || 'api-free.deepl.com' this.deeplApi = process.env.DEEPL_URL || 'api-free.deepl.com'
@@ -196,6 +198,77 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({
return { metadata, pairs } return { metadata, pairs }
}, },
async bulkTranslateMissing() {
const TARGET_LANGS = [
{ lang: 'en', field: 'anglais', deeplTarget: 'EN', suffix: '\n\n(Translated by DeepL)' },
{ lang: 'es', field: 'espagnol', deeplTarget: 'ES', suffix: '\n\n(Traducido por DeepL)' },
{ lang: 'de', field: 'allemand', deeplTarget: 'DE', suffix: '\n\n(Übersetzt von DeepL)' },
{ lang: 'it', field: 'italien', deeplTarget: 'IT', suffix: '\n\n(Tradotto da DeepL)' },
]
const pageSize = 100
let start = 0
const all = []
while (true) {
const batch = await strapi.documents('api::parole.parole').findMany({
status: 'published',
populate: ['traductions'],
fields: ['documentId', 'slug', 'titre', 'transcription', 'langueSource'],
limit: pageSize,
start,
})
all.push(...batch)
if (batch.length < pageSize) break
start += pageSize
}
const translator = new Translator()
const translated = []
const skipped = []
const errors = []
for (const parole of all) {
const sourceFR = parole.traductions?.francais
|| (parole.langueSource === 'fr' ? parole.transcription : null)
if (!sourceFR) { skipped.push(parole.slug); continue }
const missing = TARGET_LANGS.filter(({ field }) => !parole.traductions?.[field])
if (missing.length === 0) { skipped.push(parole.slug); continue }
const { id: _id, ...tradData } = parole.traductions || {}
const updatedTrad = { ...tradData }
const addedLangs = []
for (const { lang, field, deeplTarget, suffix } of missing) {
try {
await sleep(700)
const result = await translator.get('FR', deeplTarget, sourceFR)
const text = result?.translations?.[0]?.text
if (text) {
updatedTrad[field] = text + suffix
addedLangs.push(lang)
}
} catch (err) {
errors.push({ slug: parole.slug, lang: deeplTarget, error: err.message })
}
}
if (addedLangs.length > 0) {
await strapi.documents('api::parole.parole').update({
documentId: parole.documentId,
data: { traductions: updatedTrad },
})
await strapi.documents('api::parole.parole').publish({
documentId: parole.documentId,
})
translated.push({ slug: parole.slug, langs: addedLangs })
}
}
return { translated, skipped, errors }
},
parolesDiff(titre = '', oldString, newString) { parolesDiff(titre = '', oldString, newString) {
const patch = Diff.createPatch(titre, oldString, newString, 'supprimée', 'ajoutée') const patch = Diff.createPatch(titre, oldString, newString, 'supprimée', 'ajoutée')
const parsePatch = Diff.parsePatch(patch) const parsePatch = Diff.parsePatch(patch)
+38
View File
@@ -0,0 +1,38 @@
{
"collectionName": "components_social_rezo_sosyal",
"info": {
"displayName": "Rézo Sosyal",
"icon": "earth",
"description": ""
},
"options": {},
"attributes": {
"plateforme": {
"type": "enumeration",
"enum": [
"Mastodon",
"Peertube",
"Pixelfed",
"Funkwhale",
"Bluesky",
"Instagram",
"Youtube",
"Tiktok",
"Spotify",
"Deezer",
"Applemusic",
"Bandcamp",
"Soundcloud",
"Facebook",
"Twitter",
"Linktree",
"SiteWeb"
],
"required": true
},
"url": {
"type": "string",
"required": true
}
}
}
+35
View File
@@ -25,6 +25,40 @@ export interface DifferenceParolesDiff extends Struct.ComponentSchema {
}; };
} }
export interface SocialRezoSosyal extends Struct.ComponentSchema {
collectionName: 'components_social_rezo_sosyal';
info: {
description: '';
displayName: 'R\u00E9zo Sosyal';
icon: 'earth';
};
attributes: {
plateforme: Schema.Attribute.Enumeration<
[
'Mastodon',
'Peertube',
'Pixelfed',
'Funkwhale',
'Bluesky',
'Instagram',
'Youtube',
'Tiktok',
'Spotify',
'Deezer',
'Applemusic',
'Bandcamp',
'Soundcloud',
'Facebook',
'Twitter',
'Linktree',
'SiteWeb',
]
> &
Schema.Attribute.Required;
url: Schema.Attribute.String & Schema.Attribute.Required;
};
}
export interface StoreAlbum extends Struct.ComponentSchema { export interface StoreAlbum extends Struct.ComponentSchema {
collectionName: 'components_store_albums'; collectionName: 'components_store_albums';
info: { info: {
@@ -84,6 +118,7 @@ declare module '@strapi/strapi' {
export module Public { export module Public {
export interface ComponentSchemas { export interface ComponentSchemas {
'difference.paroles-diff': DifferenceParolesDiff; 'difference.paroles-diff': DifferenceParolesDiff;
'social.rezo-sosyal': SocialRezoSosyal;
'store.album': StoreAlbum; 'store.album': StoreAlbum;
'trad.traductions': TradTraductions; 'trad.traductions': TradTraductions;
'url.liens': UrlLiens; 'url.liens': UrlLiens;
+7
View File
@@ -448,6 +448,8 @@ export interface ApiArtisteArtiste extends Struct.CollectionTypeSchema {
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private; Schema.Attribute.Private;
dateNaissance: Schema.Attribute.Date; dateNaissance: Schema.Attribute.Date;
isExclusiveArtist: Schema.Attribute.Boolean &
Schema.Attribute.DefaultTo<false>;
locale: Schema.Attribute.String & Schema.Attribute.Private; locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation< localizations: Schema.Attribute.Relation<
'oneToMany', 'oneToMany',
@@ -460,7 +462,9 @@ export interface ApiArtisteArtiste extends Struct.CollectionTypeSchema {
photo: Schema.Attribute.Media<'images'>; photo: Schema.Attribute.Media<'images'>;
prenom: Schema.Attribute.String; prenom: Schema.Attribute.String;
publishedAt: Schema.Attribute.DateTime; publishedAt: Schema.Attribute.DateTime;
rezoSosyal: Schema.Attribute.Component<'social.rezo-sosyal', true>;
slug: Schema.Attribute.String; slug: Schema.Attribute.String;
titrePhare: Schema.Attribute.Relation<'manyToOne', 'api::parole.parole'>;
updatedAt: Schema.Attribute.DateTime; updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private; Schema.Attribute.Private;
@@ -540,6 +544,9 @@ export interface ApiParoleParole extends Struct.CollectionTypeSchema {
>; >;
forceSlug: Schema.Attribute.Boolean; forceSlug: Schema.Attribute.Boolean;
gadeEmbed: Schema.Attribute.String; gadeEmbed: Schema.Attribute.String;
isNewRelease: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
karaokeDesktopUrl: Schema.Attribute.String;
karaokeUrl: Schema.Attribute.String;
langueSource: Schema.Attribute.Enumeration< langueSource: Schema.Attribute.Enumeration<
['ka', 'fr', 'en', 'es', 'de', 'it'] ['ka', 'fr', 'en', 'es', 'de', 'it']
> & > &