openapi: 3.1.0
info:
  title: Unmute API
  description: API REST pour l'application de découverte musicale Unmute. Agrège des posts de sources musicales curatoriales (Bluesky).
  version: 1.0.0
  contact:
    name: Unmute
    url: https://unmute.fr

servers:
  - url: https://api.unmute.fr
    description: Production
  - url: http://localhost:8888
    description: Développement local

paths:
  /posts/:
    get:
      summary: Lister les posts musicaux
      description: |
        Endpoint principal. Retourne les posts avec filtrage, pagination et enrichissement (tags, sketchy, artistes similaires).
        Note métier : le filtre `sources` est ignoré si `tags` ou `search` sont actifs.
      parameters:
        - name: search
          in: query
          description: Recherche texte libre (nom d'artiste ou texte du post)
          schema:
            type: string
        - name: postId
          in: query
          description: Récupérer un post spécifique (ajoute les artistes similaires à la réponse)
          schema:
            type: string
        - name: sources
          in: query
          description: Filtrer par sources (séparées par virgule). Ignoré si search ou tags sont actifs.
          schema:
            type: string
            example: "adhavetFM,cmdb"
        - name: tags
          in: query
          description: Filtrer par IDs de tags (séparés par virgule)
          schema:
            type: string
            example: "1,5,12"
        - name: bookmarkUser
          in: query
          description: ID utilisateur pour n'afficher que ses bookmarks
          schema:
            type: integer
        - name: onlyNewUser
          in: query
          description: ID utilisateur pour ajouter l'info "déjà vu" (champ viewed)
          schema:
            type: integer
        - name: country
          in: query
          description: Filtrer par code pays de l'artiste
          schema:
            type: string
            example: "FR"
        - name: artistId
          in: query
          description: Filtrer par ID d'artiste (retourne uniquement les posts de cet artiste)
          schema:
            type: integer
        - name: page
          in: query
          description: Numéro de page (défaut 1)
          schema:
            type: integer
            default: 1
            minimum: 1
        - name: limit
          in: query
          description: Nombre de résultats par page (défaut 100, max 100)
          schema:
            type: integer
            default: 100
            maximum: 100
      responses:
        "200":
          description: Liste paginée de posts
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PostsResponse"
        "405":
          description: Méthode non autorisée

  /random/:
    get:
      summary: Obtenir un post aléatoire
      description: Retourne un post aléatoire non masqué avec sa position (page) dans le feed de sa source.
      responses:
        "200":
          description: Post aléatoire
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
                    example: 1
                  results:
                    type: array
                    items:
                      $ref: "#/components/schemas/Post"

  /count/:
    get:
      summary: Nombre de posts par source
      description: Retourne un objet avec le nombre de posts (non masqués) pour chaque source.
      responses:
        "200":
          description: Comptage par source
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: integer
                example:
                  adhavetFM: 1523
                  cmdb: 892
                  rhimradio: 456

  /bookmark/:
    get:
      summary: Lister les bookmarks d'un utilisateur
      parameters:
        - name: userId
          in: query
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Liste des bookmarks
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    postId:
                      type: string
                    source:
                      type: string
                    bookmarked_at:
                      type: string
                      format: date-time
    post:
      summary: Ajouter un bookmark
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userId, postId]
              properties:
                userId:
                  type: integer
                postId:
                  type: string
      responses:
        "200":
          description: Succès
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
    delete:
      summary: Supprimer un bookmark
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userId, postId]
              properties:
                userId:
                  type: integer
                postId:
                  type: string
      responses:
        "200":
          description: Succès
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"

  /view/:
    get:
      summary: Lister les posts vus par un utilisateur
      parameters:
        - name: userId
          in: query
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Liste des vues
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    postId:
                      type: string
                    source:
                      type: string
                    viewed_at:
                      type: string
                      format: date-time
    post:
      summary: Marquer un post comme vu
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userId, postId]
              properties:
                userId:
                  type: integer
                postId:
                  type: string
      responses:
        "200":
          description: Succès
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
    delete:
      summary: Supprimer une vue
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userId, postId]
              properties:
                userId:
                  type: integer
                postId:
                  type: string
      responses:
        "200":
          description: Succès
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"

  /user/create/:
    post:
      summary: Créer un compte utilisateur
      description: Crée un compte et envoie le mot de passe par email.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
      responses:
        "201":
          description: Compte créé ou déjà existant
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  email:
                    type: string
                  password:
                    type: string
                    description: Mot de passe généré (4 caractères)
                  id:
                    type: integer
                  error:
                    type: string
                    enum: [exists]

  /user/login/:
    post:
      summary: Connexion utilisateur
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
      responses:
        "201":
          description: Connexion réussie
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  id:
                    type: integer
                  email:
                    type: string
                  gridMode:
                    type: string
                  counts:
                    type: object
        "401":
          description: Identifiants invalides
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: string
                    example: not_found

  /user/update/:
    post:
      summary: Mettre à jour les préférences utilisateur
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id:
                  type: integer
                gridMode:
                  type: string
                counts:
                  type: object
      responses:
        "200":
          description: Mise à jour réussie
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  id:
                    type: integer

  /stats/:
    get:
      summary: Statistiques par source
      description: |
        Retourne les statistiques complètes pour toutes les sources : KPIs, évolution mensuelle, 
        répartition par jour de semaine, par heure, pays, tags, et évolution des tags.
        Note : ce service utilise encore mysqli (non migré vers bootstrap).
      responses:
        "200":
          description: Statistiques par source
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  $ref: "#/components/schemas/SourceStats"

  /calendar/:
    get:
      summary: Posts par jour (vue calendrier)
      description: Retourne les posts groupés par date pour un mois donné.
      parameters:
        - name: year
          in: query
          description: Année (prioritaire avec month)
          schema:
            type: integer
            example: 2025
        - name: month
          in: query
          description: Mois 1-12 (prioritaire avec year)
          schema:
            type: integer
            minimum: 1
            maximum: 12
        - name: source
          in: query
          description: Filtrer par source
          schema:
            type: string
        - name: startDate
          in: query
          description: Date de début (YYYY-MM-DD). Ignoré si year+month fournis.
          schema:
            type: string
            format: date
        - name: endDate
          in: query
          description: Date de fin (YYYY-MM-DD). Ignoré si year+month fournis.
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Posts groupés par jour
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: object
                  properties:
                    posts:
                      type: array
                      items:
                        $ref: "#/components/schemas/CalendarPost"
                    count:
                      type: integer

  /tags/:
    get:
      summary: Tags et pays par source
      description: Retourne les tags et pays disponibles pour chaque source, triés par nombre de posts.
      responses:
        "200":
          description: Tags et pays par source
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: object
                  properties:
                    tags:
                      type: array
                      items:
                        type: object
                        properties:
                          id:
                            type: integer
                          name:
                            type: string
                          post_count:
                            type: integer
                    countries:
                      type: array
                      items:
                        type: object
                        properties:
                          name:
                            type: string
                          post_count:
                            type: integer

  /artist/:
    get:
      summary: Rechercher et lister les artistes
      description: Retourne une liste paginée d'artistes, avec recherche par nom et filtrage par pays. Si `id` est fourni, retourne un artiste spécifique avec ses tags.
      parameters:
        - name: id
          in: query
          description: ID d'un artiste spécifique (retourne l'artiste avec ses tags)
          schema:
            type: integer
        - name: search
          in: query
          description: Recherche par nom d'artiste
          schema:
            type: string
        - name: country
          in: query
          description: Filtrer par code pays
          schema:
            type: string
        - name: valid
          in: query
          description: Filtrer par statut de validation (0 = non validé, 1 = validé)
          schema:
            type: integer
            enum: [0, 1]
        - name: hasPost
          in: query
          description: "Si 1, ne retourne que les artistes ayant au moins un post (postCount > 0)"
          schema:
            type: integer
            enum: [0, 1]
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
      responses:
        "200":
          description: Liste paginée d'artistes ou artiste unique
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
                  total:
                    type: integer
                  page:
                    type: integer
                  limit:
                    type: integer
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        name:
                          type: string
                        country:
                          type: string
                        mbid:
                          type: string
                        summary:
                          type: string
                        valid:
                          type: integer
                        postCount:
                          type: integer
    post:
      summary: Mettre à jour un artiste
      description: Modifie les champs d'un artiste existant. Requiert l'id et au moins un champ à modifier. Le champ `tags` (tableau de noms) remplace tous les tags existants de l'artiste.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id:
                  type: integer
                name:
                  type: string
                country:
                  type: string
                summary:
                  type: string
                summary_fr:
                  type: string
                  description: Résumé de l'artiste en français
                mbid:
                  type: string
                bc_location:
                  type: string
                bc_summary:
                  type: string
                valid:
                  type: integer
                  enum: [0, 1]
                tags:
                  type: array
                  description: Liste de noms de tags (remplace tous les tags existants). Les tags inconnus sont créés avec hide=2.
                  items:
                    type: string
                  example: ["black metal", "atmospheric", "dungeon synth"]
      responses:
        "200":
          description: Mise à jour réussie
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "400":
          description: Paramètres invalides
        "404":
          description: Artiste non trouvé

  /recommend/:
    get:
      summary: Recommandations d'artistes similaires
      description: |
        Calcule un score de similarité basé sur les relations artiste-artiste, 
        les tags communs et le pays. Non intégré côté frontend.
      parameters:
        - name: artistId
          in: query
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Artistes recommandés
          content:
            application/json:
              schema:
                type: object
                properties:
                  sourceArtist:
                    type: object
                    properties:
                      id:
                        type: integer
                      name:
                        type: string
                      country:
                        type: string
                      tags:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: integer
                            name:
                              type: string
                  recommendations:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        name:
                          type: string
                        country:
                          type: string
                        summary:
                          type: string
                        mbid:
                          type: string
                        postCount:
                          type: integer
                        similarity_score:
                          type: integer
                  count:
                    type: integer
        "400":
          description: Paramètre artistId manquant
        "404":
          description: Artiste non trouvé

components:
  schemas:
    Post:
      type: object
      properties:
        postId:
          type: string
        postDate:
          type: string
          format: date-time
        postUrl:
          type: string
        source:
          type: string
        text:
          type: string
        link:
          type: string
          nullable: true
        externalId:
          type: string
          nullable: true
        channel:
          type: string
          nullable: true
        colors:
          type: string
          nullable: true
        hide:
          type: integer
        artistId:
          type: integer
        artistName:
          type: string
        artistMbid:
          type: string
          nullable: true
        artistCountry:
          type: string
          nullable: true
        artistSummary:
          type: string
          nullable: true
        tags:
          type: array
          items:
            type: object
            properties:
              id:
                type: integer
              name:
                type: string
        sketchy:
          type: array
          items:
            type: object
            properties:
              id:
                type: integer
              name:
                type: string
              score:
                type: integer
              description:
                type: string
        viewed:
          type: string
          format: date-time
          nullable: true
          description: Présent uniquement si onlyNewUser est fourni
        relatedArtists:
          type: array
          nullable: true
          description: Présent uniquement si postId est fourni
          items:
            type: object
            properties:
              id:
                type: integer
              name:
                type: string
              country:
                type: string
              summary:
                type: string
              postCount:
                type: integer

    PostsResponse:
      type: object
      properties:
        count:
          type: integer
          description: Nombre de résultats dans cette page
        total:
          type: integer
          description: Total de résultats correspondant aux filtres
        page:
          type: integer
        limit:
          type: integer
        results:
          type: array
          items:
            $ref: "#/components/schemas/Post"

    CalendarPost:
      type: object
      properties:
        postId:
          type: string
        postDate:
          type: string
          format: date-time
        postUrl:
          type: string
        source:
          type: string
        text:
          type: string
        link:
          type: string
          nullable: true
        colors:
          type: string
          nullable: true
        artistId:
          type: integer
        artistName:
          type: string
        artistCountry:
          type: string
          nullable: true
        artistMbid:
          type: string
          nullable: true

    SourceStats:
      type: object
      properties:
        kpis:
          type: object
          properties:
            total_posts:
              type: integer
            total_countries:
              type: integer
            total_tags:
              type: integer
            total_artists:
              type: integer
        evolution:
          type: object
          description: Chart.js format (labels + datasets par année)
        by_weekday:
          type: object
          description: Chart.js format (7 jours)
        by_hour:
          type: object
          description: Chart.js format (24h)
        countries_global:
          type: object
          description: Chart.js format (top 30 pays)
        tags_global:
          type: object
          description: Chart.js format (top 30 tags)
        tags_evolution:
          type: object
          description: Chart.js format (top 10 tags + Autres, mensuel)

    SuccessResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
