diff --git a/README.org b/README.org index 7746a88..97c42fb 100644 --- a/README.org +++ b/README.org @@ -43,8 +43,9 @@ The library is implemented around a set of entities. | *Name* | *Type* | *Fields* | *Description* | |------------------+-----------+----------------------+---------------------------------------------------------| | song | structure | name, album, file, … | | -| album | structure | name, artist | | +| album | structure | name, date, artists | | | artist | structure | name | | +| genre | structure | name | | | directory | structure | name, path | | | stored-playlist | structure | name | A named user-specified sequence of songs | | search-criteria | structure | type, what | Read the [[https://www.musicpd.org/doc/protocol/database.html][protocol documentation]] | @@ -52,6 +53,7 @@ The library is implemented around a set of entities. |------------------+-----------+----------------------+---------------------------------------------------------| | artists | symbol | /none/ | Represent the set of all artists | | albums | symbol | /none/ | Represent the set of all albums | +| genres | symbol | /none/ | Represent the set of all genres | | directories | symbol | /none/ | Represent all directories in ~libmpdel-music-directory~ | | current-playlist | symbol | /none/ | Represent the currently played sequence of songs | | stored-playlists | symbol | /none/ | Represent the set of all stored playlists | diff --git a/libmpdel.el b/libmpdel.el index 5db54ba..5f29f4e 100644 --- a/libmpdel.el +++ b/libmpdel.el @@ -33,6 +33,7 @@ (require 'tq) (require 'cl-lib) (require 'subr-x) +(require 'seq) ;;; Customization @@ -171,7 +172,8 @@ message from the server.") (:constructor libmpdel--album-create) (:conc-name libmpdel--album-)) (name nil :read-only t) - (artist nil :read-only t)) + (date nil :read-only t) + (artists nil :read-only t)) (cl-defstruct (libmpdel-song (:constructor libmpdel--song-create) @@ -180,6 +182,9 @@ message from the server.") (track nil :read-only t) (file nil :read-only t) (album nil :read-only t) + (genres nil :read-only t) + (performers nil :read-only t) + (artists nil :read-only t) (disc nil :read-only t) (date nil :read-only t) (id nil :read-only t) @@ -190,6 +195,11 @@ message from the server.") (:conc-name libmpdel--stored-playlist-)) (name nil :read-only t)) +(cl-defstruct (libmpdel-genre + (:constructor libmpdel--genre-create) + (:conc-name libmpdel--genre-)) + (name nil :read-only t)) + (cl-defstruct (libmpdel-search-criteria (:constructor libmpdel-search-criteria-create) (:conc-name libmpdel--search-criteria-)) @@ -205,20 +215,28 @@ message from the server.") "Return artist name of ENTITY." (libmpdel--artist-name (libmpdel-artist entity))) +(defun libmpdel-artists-name (entity) + "Return semicolon separated string of artists names of ENTITY." + (string-join (mapcar #'libmpdel--artist-name + (libmpdel-artists entity)) + "; ")) + (cl-defgeneric libmpdel-artist (entity) - "Return artist of ENTITY.") + "Return artist of ENTITY." + (or (car (libmpdel-artists entity)) + libmpdel--unknown-artist)) -(cl-defmethod libmpdel-artist ((artist libmpdel-artist)) - "Return ARTIST." - artist) +(cl-defmethod libmpdel-artists ((artist libmpdel-artist)) + "Return singleton list containing ARTIST." + (list artist)) -(cl-defmethod libmpdel-artist ((album libmpdel-album)) +(cl-defmethod libmpdel-artists ((album libmpdel-album)) "Return the ALBUM's artist." - (libmpdel--album-artist album)) + (libmpdel--album-artists album)) -(cl-defmethod libmpdel-artist ((song libmpdel-song)) - "Return the SONG's artist." - (libmpdel-artist (libmpdel--song-album song))) +(cl-defmethod libmpdel-artists ((song libmpdel-song)) + "Return the SONG's artists." + (libmpdel--song-artists song)) (defun libmpdel-album-name (entity) "Return album name of ENTITY." @@ -235,6 +253,18 @@ message from the server.") "Return SONG's album." (libmpdel--song-album song)) +(cl-defmethod libmpdel-genres ((song libmpdel-song)) + "Return SONG's genres." + (libmpdel--song-genres song)) + +(cl-defmethod libmpdel-genres ((genre libmpdel-genre)) + "Return singleton list GENRE." + (list genre)) + +(cl-defmethod libmpdel-performers ((song libmpdel-song)) + "Return SONG's performers." + (libmpdel--song-performers song)) + (cl-defgeneric libmpdel-entity-name (entity) "Return the name of ENTITY.") @@ -246,6 +276,10 @@ message from the server.") "Return ALBUM's name." (libmpdel--album-name album)) +(cl-defmethod libmpdel-entity-name ((genre libmpdel-genre)) + "Return GENRE's name." + (libmpdel--genre-name genre)) + (cl-defmethod libmpdel-entity-name ((song libmpdel-song)) "Return SONG's name. @@ -265,6 +299,10 @@ If the SONG's name is nil, return the filename instead." "Return a string describing the `albums' entity." "All albums") +(cl-defmethod libmpdel-entity-name ((_entity (eql genres))) + "Return a string describing the `genres' entity." + "All genres") + (cl-defmethod libmpdel-entity-name ((_entity (eql current-playlist))) "Return a string describing the `current-playlist' entity." "Current playlist") @@ -295,6 +333,10 @@ If the SONG's name is nil, return the filename instead." "Return ALBUM's artist." (libmpdel-artist album)) +(cl-defmethod libmpdel-entity-parent ((_genre libmpdel-genre)) + "Return the `genres' entity." + 'genres) + (cl-defmethod libmpdel-entity-parent ((_artist libmpdel-artist)) "Return the `artists' entity." 'artists) @@ -321,9 +363,16 @@ If the SONG's name is nil, return the filename instead." "Return the track number of SONG within its album." (or (libmpdel--song-track song) "")) -(defun libmpdel-entity-date (song) - "Return the date of SONG." - (or (libmpdel--song-date song) "")) +(cl-defgeneric libmpdel-entity-date (entity) + "Return the date of ENTITY.") + +(cl-defmethod libmpdel-entity-date ((album libmpdel-album)) + "Return ALBUM's date." + (libmpdel--album-date album)) + +(cl-defmethod libmpdel-entity-date ((song libmpdel-song)) + "Return SONG's date." + (libmpdel--song-date song)) (defun libmpdel-song-disc (song) "Return the disc number of SONG within its album." @@ -339,15 +388,28 @@ If the SONG's name is nil, return the filename instead." (when (and (stringp pos) (not (string= pos ""))) (string-to-number pos)))) +(defun libmpdel--artists-create (artist-names) + "Return a list of artists whose names are ARTIST-NAMES." + (mapcar (lambda (name) + (libmpdel--artist-create :name name)) + artist-names)) + +(defun libmpdel--genres-create (genre-names) + "Return a list of genres whose names are GENRE-NAMES." + (mapcar (lambda (name) + (libmpdel--genre-create :name name)) + genre-names)) + (defun libmpdel--create-song-from-data (song-data) "Return a song from SONG-DATA, a server's response." (libmpdel--song-create :name (cdr (assq 'Title song-data)) :track (cdr (assq 'Track song-data)) :file (cdr (assq 'file song-data)) - :album (libmpdel--album-create - :name (cdr (assq 'Album song-data)) - :artist (libmpdel--artist-create :name (cdr (assq 'Artist song-data)))) + :genres (libmpdel--genres-create (libmpdel-entries song-data 'Genre)) + :performers (libmpdel--artists-create (libmpdel-entries song-data 'Performer)) + :artists (libmpdel--artists-create (libmpdel-entries song-data 'Artist)) + :album (libmpdel--create-album-from-data song-data) :date (cdr (assq 'Date song-data)) :disc (cdr (assq 'Disc song-data)) :id (cdr (assq 'Id song-data)) @@ -357,6 +419,17 @@ If the SONG's name is nil, return the filename instead." "Return a list of songs from DATA, a server's response." (mapcar #'libmpdel--create-song-from-data (libmpdel-group-data data))) +(defun libmpdel--create-album-from-data (album-data) + "Return an album from ALBUM-DATA, a server's response." + (libmpdel--album-create + :name (cdr (assq 'Album album-data)) + :date (cdr (assq 'Date album-data)) + :artists (libmpdel--artists-create (libmpdel-entries album-data 'AlbumArtist)))) + +(defun libmpdel--create-albums-from-data (data) + "Return a list of albums from DATA, a server's response." + (mapcar #'libmpdel--create-album-from-data (libmpdel-group-data data))) + (defun libmpdel-current-playlist-p (entity) "Return non-nil if ENTITY is the current playlist." (eq entity 'current-playlist)) @@ -920,12 +993,26 @@ If HANDLER is nil, ignore response." (cl-defmethod libmpdel-entity-to-criteria ((album libmpdel-album)) "Return search query matching all songs from ALBUM." (format "%s album %S" - (libmpdel-entity-to-criteria (libmpdel-artist album)) + (string-join + (mapcar (lambda (artist) + (format "albumartist %S" (libmpdel-entity-name artist))) + (libmpdel-artists album)) + " ") (libmpdel-entity-name album))) +(cl-defmethod libmpdel-entity-to-criteria ((genre libmpdel-genre)) + "Return search query matching all songs from GENRE." + (format "genre %S" + (libmpdel-entity-name genre))) + (cl-defmethod libmpdel-entity-to-criteria ((song libmpdel-song)) "Return search query matching SONG." - (format "%s title %S" + (format "%s %s title %S" + (string-join + (mapcar (lambda (artist) + (format "artist %S" (libmpdel-entity-name artist))) + (libmpdel-artists song)) + " ") (libmpdel-entity-to-criteria (libmpdel-album song)) (libmpdel-entity-name song))) @@ -950,14 +1037,24 @@ If HANDLER is nil, ignore response." (cl-defmethod libmpdel-list ((_entity (eql albums)) function) "Call FUNCTION with all albums as parameter." (libmpdel-send-command + ;; TODO: compute all albums by listing all songs "list album" + (lambda (data) + (funcall function + (seq-uniq + (libmpdel--create-albums-from-data data) + #'libmpdel-equal))))) + +(cl-defmethod libmpdel-list ((_entity (eql genres)) function) + "Call FUNCTION with all genres as parameter." + (libmpdel-send-command + "list genre" (lambda (data) (funcall function (mapcar - (lambda (album-name) - (libmpdel--album-create :name album-name - :artist libmpdel--unknown-artist)) - (libmpdel-sorted-entries data 'Album)))))) + (lambda (genre-name) + (libmpdel--genre-create :name genre-name)) + (libmpdel-sorted-entries data 'Genre)))))) (cl-defmethod libmpdel-list ((_entity (eql stored-playlists)) function) "Call FUNCTION with all stored playlists as parameter." @@ -972,12 +1069,12 @@ If HANDLER is nil, ignore response." (cl-defmethod libmpdel-list ((artist libmpdel-artist) function) "Call FUNCTION with all albums of ARTIST as parameter." (libmpdel-send-command - `("list album %s" ,(libmpdel-entity-to-criteria artist)) + `("find %s sort AlbumSort" ,(libmpdel-entity-to-criteria artist)) (lambda (data) (funcall function - (mapcar - (lambda (album-name) (libmpdel--album-create :name album-name :artist artist)) - (libmpdel-sorted-entries data 'Album)))))) + (seq-uniq + (libmpdel--create-albums-from-data data) + #'libmpdel-equal))))) (cl-defgeneric libmpdel-list-songs (entity function) "Call FUNCTION with all songs of ENTITY." diff --git a/test/libmpdel-test.el b/test/libmpdel-test.el index 0edad04..01be9ad 100644 --- a/test/libmpdel-test.el +++ b/test/libmpdel-test.el @@ -54,23 +54,23 @@ (ert-deftest libmpdel-test-artist () (let* ((artist (libmpdel--artist-create :name "The Artist")) - (album (libmpdel--album-create :name "The Album" :artist artist)) - (song (libmpdel--song-create :name "The song" :album album))) + (album (libmpdel--album-create :name "The Album" :artists (list artist))) + (song (libmpdel--song-create :name "The song" :album album :artists (list artist)))) (should (equal artist (libmpdel-artist artist))) (should (equal artist (libmpdel-artist album))) (should (equal artist (libmpdel-artist song))))) (ert-deftest libmpdel-test-artist-name () (let* ((artist (libmpdel--artist-create :name "The Artist")) - (album (libmpdel--album-create :name "The Album" :artist artist)) - (song (libmpdel--song-create :name "The song" :album album))) + (album (libmpdel--album-create :name "The Album" :artists (list artist))) + (song (libmpdel--song-create :name "The song" :album album :artists (list artist)))) (should (equal "The Artist" (libmpdel-artist-name artist))) (should (equal "The Artist" (libmpdel-artist-name album))) (should (equal "The Artist" (libmpdel-artist-name song))))) (ert-deftest libmpdel-test-album-name () (let* ((artist (libmpdel--artist-create :name "The Artist")) - (album (libmpdel--album-create :name "The Album" :artist artist)) + (album (libmpdel--album-create :name "The Album" :artists (list artist))) (song (libmpdel--song-create :name "The song" :album album))) (should-error (libmpdel-album-name artist)) (should (equal "The Album" (libmpdel-album-name album))) @@ -78,7 +78,7 @@ (ert-deftest libmpdel-test-album () (let* ((artist (libmpdel--artist-create :name "The Artist")) - (album (libmpdel--album-create :name "The Album" :artist artist)) + (album (libmpdel--album-create :name "The Album" :artists (list artist))) (song (libmpdel--song-create :name "The song" :album album))) (should-error (libmpdel-album artist)) (should (equal album (libmpdel-album album))) @@ -86,7 +86,7 @@ (ert-deftest libmpdel-test-entity-name () (let* ((artist (libmpdel--artist-create :name "The Artist")) - (album (libmpdel--album-create :name "The Album" :artist artist)) + (album (libmpdel--album-create :name "The Album" :artists (list artist))) (song (libmpdel--song-create :name "The song" :album album)) (stored-playlist (libmpdel--stored-playlist-create :name "The playlist"))) (should (equal "The Artist" (libmpdel-entity-name artist))) @@ -101,7 +101,7 @@ (ert-deftest libmpdel-test-entity-parent () (let* ((artist (libmpdel--artist-create :name "The Artist")) - (album (libmpdel--album-create :name "The Album" :artist artist)) + (album (libmpdel--album-create :name "The Album" :artists (list artist))) (song (libmpdel--song-create :name "The song" :album album)) (stored-playlist (libmpdel--stored-playlist-create :name "The playlist"))) (should (equal 'artists (libmpdel-entity-parent artist))) @@ -116,12 +116,19 @@ (let ((song (libmpdel--create-song-from-data '((Title . "The song") (file . "foo/song.ogg") + (Date . "1970-01-01") (Album . "The Album") + (AlbumArtist . "The Albumartist") + (Genre . "The Genre") (Artist . "The Artist"))))) (should (equal "The song" (libmpdel-entity-name song))) (should (equal "foo/song.ogg" (libmpdel-song-file song))) + (should (equal "1970-01-01" (libmpdel-entity-date song))) + (should (equal "1970-01-01" (libmpdel-entity-date (libmpdel-album song)))) + (should (equal (list "The Genre") (mapcar #'libmpdel-entity-name (libmpdel-genres song)))) (should (equal "The Album" (libmpdel-entity-name (libmpdel-album song)))) - (should (equal "The Artist" (libmpdel-entity-name (libmpdel-artist (libmpdel-album song))))))) + (should (equal "The Artist" (libmpdel-entity-name (libmpdel-artist song)))) + (should (equal "The Albumartist" (libmpdel-entity-name (libmpdel-artist (libmpdel-album song))))))) (ert-deftest libmpdel-test-current-playlist-p () (should (libmpdel-current-playlist-p 'current-playlist)) @@ -269,20 +276,22 @@ (ert-deftest libmpdel-test-playlist-add-no-string-id-sends-findadd () (let* ((artist (libmpdel--artist-create :name "The Artist")) - (album (libmpdel--album-create :name "The Album" :artist artist))) + (album (libmpdel--album-create :name "The Album" :artists (list artist)))) (libmpdel-test--with-connection (libmpdel-playlist-add album 'current-playlist) - (should (equal '("findadd artist \"The Artist\" album \"The Album\"") + (should (equal '("findadd albumartist \"The Artist\" album \"The Album\"") (last commands)))))) (ert-deftest libmpdel-test-playlist-add-sends-findadd () (let ((song (libmpdel--create-song-from-data '((Title . "S") (Album . "A") + (AlbumArtist . "Art") + (AlbumArtist . "Bart") (Artist . "Art"))))) (libmpdel-test--with-connection (libmpdel-playlist-add song 'current-playlist) - (should (equal '("findadd artist \"Art\" album \"A\" title \"S\"") + (should (equal '("findadd artist \"Art\" albumartist \"Art\" albumartist \"Bart\" album \"A\" title \"S\"") (last commands)))))) @@ -323,9 +332,9 @@ (let* ((artist1 (libmpdel--artist-create :name "artist1")) (artist1-bis (libmpdel--artist-create :name "artist1")) (artist2 (libmpdel--artist-create :name "artist2")) - (album1 (libmpdel--album-create :name "album1" :artist artist1)) - (album1-bis (libmpdel--album-create :name "album1" :artist artist1)) - (album2 (libmpdel--album-create :name "album2" :artist artist1)) + (album1 (libmpdel--album-create :name "album1" :artists (list artist1))) + (album1-bis (libmpdel--album-create :name "album1" :artists (list artist1))) + (album2 (libmpdel--album-create :name "album2" :artists (list artist1))) (song1 (libmpdel--song-create :name "name" :file "file"