feat(postgres): add music database service

Introduces a dedicated PostgreSQL database and schema for music data,
including playlists, tracks, and various analytical views. This service
is designed for importing Spotify playlists into YouTube Music.

Updates the codeberg-mcp dependency hash to SRI format. Refactors
home-manager package names for prettier and neovim-node-client.
This commit is contained in:
Hermes Agent
2026-09-24 22:55:18 -04:00
parent 9639f7ae35
commit db098efdf5
4 changed files with 125 additions and 4 deletions
+3 -2
View File
@@ -8,8 +8,9 @@ let
owner = "effecet";
repo = "codeberg-mcp";
rev = "f7e2fa06ca097cfa5638cef3768c35072c554fd9";
# Hash temporaire — à corriger avec nix-prefetch-github après build.
hash = "1glxwx1wvvvck3pmp41yxi56z138wamvp0hf3nbls3zd8w9damqj";
# Hash base32 nix-prefetch-url -> SRI : depuis Nix 2.x un hash nu n'a plus
# de type et fait échouer TOUTE l'évaluation de la config tower.
hash = "sha256-ElfVEkftD02XHQ6Cu6viaIRvSuw+kFvvmGzvzUPnnb4=";
};
# Package Python manuel : le repo n'a pas de build-system valide.
# On crée un binaire qui lance server.py avec le bon python.
+1
View File
@@ -1,6 +1,7 @@
{ pkgs, config, ... }:
{
imports = [ ./music.nix ];
sops.secrets."postgres_crt" = {
sopsFile = ../../secrets/common.yml;
+119
View File
@@ -0,0 +1,119 @@
{ pkgs, ... }:
# Base "music" : export des playlists Spotify de marc (Exportify), importées vers
# YouTube Music. Chaque titre garde ses métadonnées d'origine (artistes, album,
# ISRC, durée) et le video id YouTube Music auquel il a été apparié, ce qui permet
# de requêter les doublons entre playlists, les artistes les plus présents, ou les
# titres sans équivalent trouvé côté YouTube Music.
#
# Rôle dédié "music" (comme les autres services du repo) plutôt que le rôle
# "hermes" : ce n'est pas un besoin interne de l'agent, n'importe quel outil peut
# s'y brancher en trust local via `psql -h 127.0.0.1 -U music -d music`.
let
schema = pkgs.writeText "music-schema.sql" ''
-- Playlists importées (id = id de playlist YouTube Music).
create table if not exists playlist (
id text primary key,
name text not null,
source_file text,
privacy text not null default 'PRIVATE',
imported_at timestamptz not null default now()
);
create table if not exists track (
id bigint generated by default as identity primary key,
title text not null,
artists text[] not null default '{}',
album text,
album_artists text[],
duration_ms integer,
release_date date,
-- Spotify ne donne que l'annee pour certaines sorties : release_date est
-- alors normalisee au 1er janvier et la precision reelle est conservee ici.
release_date_precision text,
isrc text,
explicit boolean,
spotify_track_id text unique,
youtube_music_video_id text
);
-- Pour une base deja creee avant l'ajout d'une colonne (create table if not
-- exists ne touche pas une table existante).
alter table track add column if not exists release_date_precision text;
-- Ordre exact des titres dans chaque playlist (position = index d'origine).
create table if not exists playlist_track (
playlist_id text not null references playlist(id) on delete cascade,
position integer not null,
track_id bigint not null references track(id) on delete cascade,
added_at timestamptz,
primary key (playlist_id, position)
);
-- Titres likés sur YouTube Music (ex "Liked Songs" de Spotify).
create table if not exists liked_track (
track_id bigint primary key references track(id) on delete cascade,
liked_at timestamptz
);
create index if not exists track_title_lower_idx on track (lower(title));
create index if not exists track_artists_idx on track using gin (artists);
create index if not exists playlist_track_track_idx on playlist_track (track_id);
create or replace view v_playlist_stats as
select p.id, p.name, p.privacy, count(pt.track_id) as tracks
from playlist p
left join playlist_track pt on pt.playlist_id = p.id
group by p.id, p.name, p.privacy
order by p.name;
-- Titres présents dans plusieurs playlists.
create or replace view v_duplicate_tracks as
select t.id, t.title, t.artists, count(distinct pt.playlist_id) as playlists
from track t
join playlist_track pt on pt.track_id = t.id
group by t.id, t.title, t.artists
having count(distinct pt.playlist_id) > 1
order by playlists desc, t.title;
create or replace view v_top_artists as
select artist, count(distinct t.id) as tracks
from track t
cross join lateral unnest(t.artists) as artist
group by artist
order by tracks desc;
-- Titres sans équivalent trouvé sur YouTube Music.
create or replace view v_unmatched_tracks as
select id, title, artists, album
from track
where youtube_music_video_id is null
order by title;
'';
in {
services.postgresql.ensureDatabases = [ "music" ];
services.postgresql.ensureUsers = [
{
name = "music";
ensureDBOwnership = true;
}
];
# Schéma appliqué à chaque activation. Tout est en IF NOT EXISTS / OR REPLACE,
# donc idempotent : une base vidée ou restaurée se répare toute seule, et une
# base déjà remplie n'est jamais touchée (aucun DROP).
systemd.services.music-schema = {
description = "Applique le schéma de la base music";
requires = [ "postgresql-setup.service" ];
after = [ "postgresql-setup.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
${pkgs.postgresql_17}/bin/psql -h 127.0.0.1 -U music -d music \
-v ON_ERROR_STOP=1 -q -f ${schema}
'';
};
}
+2 -2
View File
@@ -50,14 +50,14 @@
ripgrep # telescope live-grep + vim.health
fd # telescope extended
stylua # conform lua formatter
nodePackages.prettier # conform prettier
prettier # conform prettier
tree-sitter # lspsaga + nvim-treesitter CLI
sox # gp.nvim audio
pngpaste # img-clip (macOS)
findutils # GNU find (fix fzf-lua "illegal option")
luarocks # lazy.nvim rocks
nodePackages.neovim # node provider
neovim-node-client # node provider
# (nerdfonts.override { fonts = [ "Meslo" ]; })
# ou sur nixpkgs récent :