package shakuhachi

  1. Overview
  2. Docs
A music collection manager

Install

dune-project
 Dependency

Authors

Maintainers

Sources

0.3.2.tar.gz
sha256=c72807d303de137441b643fec1c7630c930ebc792b576783c7961b237c2114db
sha512=770f878bbdac8033d90e904ce8e9c84fc75331c7ff9b35e2204fafe3746f432f2664296dac91942a788b82a2da07478d91b76ed85cd880758d2def8a55144840

doc/src/shakuhachi.database/albumParticipant.ml.html

Source file albumParticipant.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
(*****************************************************************************)
(*                                                                           *)
(*  Copyright (C) 2026 Yves Ndiaye                                           *)
(*                                                                           *)
(* This Source Code Form is subject to the terms of the Mozilla Public       *)
(* License, v. 2.0. If a copy of the MPL was not distributed with this       *)
(* file, You can obtain one at https://mozilla.org/MPL/2.0/.                 *)
(*                                                                           *)
(*****************************************************************************)

type t = { album_id : Int64.t; artist_id : Int64.t; role : Role.t }

module Column = struct
  let album_id = "album_id"
  let artist_id = "artist_id"
  let role = "role"
end

let table_name = "album_participants"
let foreign_album_id = Column.album_id
let foreign_artist_id = Column.artist_id
let key_role = Column.role
let p_album_id = Printf.sprintf "%s.%s" table_name foreign_album_id
let p_artist_id = Printf.sprintf "%s.%s" table_name foreign_artist_id
let p_role = Printf.sprintf "%s.%s" table_name key_role

let sql_table =
  Printf.sprintf
    {|CREATE TABLE IF NOT EXISTS %s (
      %s INTEGER NOT NULL,
      %s INTEGER NOT NULL,
      %s INTEGER,
      UNIQUE (%s, %s, %s),
      FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE CASCADE ON DELETE CASCADE
      FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE CASCADE ON DELETE CASCADE
  );|}
    table_name foreign_album_id foreign_artist_id key_role foreign_album_id
    foreign_artist_id key_role foreign_album_id Album.table_name
    Album.primary_key foreign_artist_id Artist.table_name Artist.primary_key

let init ~album_id ~artist_id role = { album_id; artist_id; role }

let update_artist database old new' =
  let ( let* ) rc f =
    match rc with
    | Sqlite3.Rc.OK | Sqlite3.Rc.DONE ->
        f ()
    | rc ->
        let message = Sqlite3.errmsg database in
        Error (rc, message)
  in
  let statement =
    Printf.sprintf "UPDATE %s SET %s = ?1 WHERE %s = ?2;" table_name
      foreign_artist_id foreign_artist_id
  in
  let statement = Sqlite3.prepare database statement in
  let* () = Sqlite3.bind_int64 statement 1 new' in
  let* () = Sqlite3.bind_int64 statement 2 old in
  match Sqlite3.step statement with
  | Sqlite3.Rc.OK | Sqlite3.Rc.DONE | Sqlite3.Rc.CONSTRAINT ->
      let* () = Sqlite3.finalize statement in
      Ok ()
  | rc ->
      let message = Sqlite3.errmsg database in
      Error (rc, message)

let columns statement offset =
  let album_id = Sqlite3.column_int64 statement offset in
  let artist_id = Sqlite3.column_int64 statement (offset + 1) in
  let role = Role.of_int (Sqlite3.column_int statement (offset + 2)) in
  ({ album_id; artist_id; role }, offset + 3)

let remove database t =
  let ( let* ) = Util.bind_ok database in
  let { album_id; artist_id; role } = t in
  let statement =
    Printf.sprintf "DELETE FROM %s WHERE (%s = ?) AND (%s = ?) AND (%s = ?);"
      table_name Column.album_id Column.artist_id Column.role
  in
  let statement = Sqlite3.prepare database statement in
  let* () = Sqlite3.bind_int64 statement 1 album_id in
  let* () = Sqlite3.bind_int64 statement 2 artist_id in
  let* () = Sqlite3.bind_int statement 3 (Role.to_int role) in
  match Sqlite3.step statement with
  | Sqlite3.Rc.OK | Sqlite3.Rc.DONE ->
      let* () = Sqlite3.finalize statement in
      Ok ()
  | rc ->
      let message = Sqlite3.errmsg database in
      Error (rc, message)

let insert database t =
  let { album_id; artist_id; role } = t in
  let statement' =
    Printf.sprintf "INSERT INTO %s (%s, %s, %s) VALUES (?, ?, ?);" table_name
      Column.album_id Column.artist_id Column.role
  in
  let ( let* ) rc f =
    match rc with
    | Sqlite3.Rc.OK | Sqlite3.Rc.DONE ->
        f ()
    | rc ->
        let message = Sqlite3.errmsg database in
        Error (rc, message)
  in
  let statement = Sqlite3.prepare database statement' in
  let* () = Sqlite3.bind_int64 statement 1 album_id in
  let* () = Sqlite3.bind_int64 statement 2 artist_id in
  let* () = Sqlite3.bind_int statement 3 (Role.to_int role) in
  match Sqlite3.step statement with
  | Sqlite3.Rc.OK | Sqlite3.Rc.DONE | Sqlite3.Rc.CONSTRAINT ->
      let* () = Sqlite3.finalize statement in
      Ok ()
  | rc ->
      let message = Sqlite3.errmsg database in
      Error (rc, message)

let filter_map database f =
  let statement =
    Printf.sprintf "SELECT %s, %s, %s FROM %s;" Column.album_id Column.artist_id
      Column.role table_name
  in
  let statement = Sqlite3.prepare database statement in
  let queue = Queue.create () in
  let rc =
    Sqlite3.iter statement ~f:(fun _ ->
        let participant, _ = columns statement 0 in
        match f participant with None -> () | Some e -> Queue.add e queue
    )
  in
  let values = queue |> Queue.to_seq |> List.of_seq in
  Util.bind_ok database rc @@ fun () -> Ok values

let filter_map_with_album_id database album_id f =
  let ( let* ) = Util.bind_ok database in
  let statement =
    Printf.sprintf "SELECT %s, %s, %s FROM %s WHERE %s = ?;" Column.album_id
      Column.artist_id Column.role table_name Column.album_id
  in
  let statement = Sqlite3.prepare database statement in
  let* () = Sqlite3.bind_int64 statement 1 album_id in
  let queue = Queue.create () in
  let rc =
    Sqlite3.iter statement ~f:(fun _ ->
        let participant, _ = columns statement 0 in
        match f participant with None -> () | Some e -> Queue.add e queue
    )
  in
  let values = queue |> Queue.to_seq |> List.of_seq in
  Util.bind_ok database rc @@ fun () -> Ok values

(** [find ~compilation database artists album] tries to find an album in
    [database] with the name [album.name] and at least an arist with the name is
    in [artists] unless [compilation], in this case only the [album.name] is
    check to discriminate albums *)
let find ~compilation database (artists : (Artist.t * Role.t) list)
    (album : Album.t) =
  let module Albums = Set.Make (struct
    type t = Album.t

    let compare (lhs : Album.t) (rhs : Album.t) = compare lhs.id rhs.id
  end) in
  let query =
    Printf.sprintf
      "SELECT %s.*, %s FROM %s INNER JOIN %s ON %s = %s INNER JOIN %s ON %s = \
       %s;"
      Album.table_name Artist.p_name table_name Album.table_name p_album_id
      Album.p_id Artist.table_name p_artist_id Artist.p_id
  in
  let statement = Sqlite3.prepare database query in

  let _, albums =
    Sqlite3.fold statement ~init:Albums.empty ~f:(fun albums _ ->
        let qalbum, offset = Album.columns statement 0 in
        let artist_name = Sqlite3.column_text statement offset in
        let artists () =
          compilation
          || List.exists
               (fun ((artist : Artist.t), _) -> artist.name = artist_name)
               artists
        in
        let matched = qalbum.name = album.name && artists () in
        match matched with true -> Albums.add qalbum albums | false -> albums
    )
  in
  Albums.elements albums

let find_from_metadata ~compilation database metadata =
  let module Albums = Set.Make (struct
    type t = Album.t

    let compare (lhs : Album.t) (rhs : Album.t) = compare lhs.id rhs.id
  end) in
  let album_name = SkhcMetadata.Metadata.album metadata in
  let artists =
    SkhcMetadata.Metadata.artists metadata
    @ SkhcMetadata.Metadata.album_artists metadata
  in
  let query =
    Printf.sprintf
      "SELECT %s.*, %s FROM %s INNER JOIN %s ON %s = %s INNER JOIN %s ON %s = \
       %s;"
      Album.table_name Artist.p_name table_name Album.table_name p_album_id
      Album.p_id Artist.table_name p_artist_id Artist.p_id
  in
  let statement = Sqlite3.prepare database query in

  let _, albums =
    Sqlite3.fold statement ~init:Albums.empty ~f:(fun albums _ ->
        let qalbum, offset = Album.columns statement 0 in
        let artist_name = Sqlite3.column_text statement offset in
        let artists () =
          compilation || List.exists (( = ) artist_name) artists
        in
        let matched = Some qalbum.name = album_name && artists () in
        match matched with true -> Albums.add qalbum albums | false -> albums
    )
  in
  Albums.elements albums

let insert_multitple database (album : Album.t)
    (artists : (Artist.t * Role.t) list) =
  List.iter
    (fun ((artist : Artist.t), role) ->
      let t = { album_id = album.id; artist_id = artist.id; role } in
      match insert database t with Ok () -> () | Error _e -> ()
    )
    artists

(** [find_or_insert ~compilation database artists album] tries to find an album
    in [database] with the name [album.name] and at least an artist with the
    name is in [artists] unless [compilation], in this case only the
    [album.name] is check to discriminate albums. If no album is found,
    [find_or_insert] will try to insert [album] to [database].

    If [insert_on_found], [find_or_insert] will try to add [artists] to the
    already existing album.

    @before 0.3.2,
      no [insert_on_found] optional argument and the behavior of
      [find_or_insert] was equilatent to [insert_on_found = false]. *)
let find_or_insert ?(insert_on_found = false) ~compilation database artists
    (album : Album.t) =
  let ( let* ) = Result.bind in
  let albums = find ~compilation database artists album in
  match albums with
  | [] ->
      let* album = Album.insert database album in
      let () = insert_multitple database album artists in
      Ok album
  | album :: _ ->
      let () =
        if insert_on_found then
          insert_multitple database album artists
      in
      Ok album

let album_id t = t.album_id
let artist_id t = t.artist_id
let role t = t.role

(* Test *)
let equal : t -> t -> bool = ( = )
let compare : t -> t -> int = Stdlib.compare