-
Notifications
You must be signed in to change notification settings - Fork 15
/
vuiet.el
1073 lines (949 loc) · 39.9 KB
/
vuiet.el
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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; vuiet.el --- The music player and explorer for Emacs -*- lexical-binding: t -*-
;; Copyright (C) 2019-2024 Mihai Olteanu
;; Author: Mihai Olteanu <[email protected]>
;; Version: 1.0
;; Package-Requires: ((emacs "26.1") (lastfm "1.1") (versuri "1.0") (s "1.12.0") (bind-key "2.4") (mpv "0.1.0") (ivy "0.14.2"))
;; Keywords: multimedia
;; URL: https://github.com/mihaiolteanu/vuiet
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Vuiet is a music player and explorer for Emacs users. It is similar in scope
;; to lastfm on which it is based. All tracks are played from youtube using mpv
;; in the background and music info taken from last.fm. Vuiet supports the
;; "discovery mode", where it lets you create your own playlists based on
;; artist, genre or your loved songs similarities. Or, you can specify single
;; tracks, top tracks from a given artist, known albums, etc. There is also a
;; lyrics database that is optionally updated with each track you play and this
;; database can be searched interactively and played from.
;;; Code:
(require 'lastfm)
(require 'versuri)
(require 's)
(require 'cl-lib)
(require 'bind-key)
(require 'mpv)
(require 'generator)
(require 'ivy)
(defgroup vuiet ()
"Emacs music player."
:group 'music
:prefix "vuiet-")
(defcustom vuiet-scrobble-timeout 30
"Time, in seconds, for the same song to play before scrobbling it."
:type '(number :tag "seconds")
:group 'vuiet)
(defcustom vuiet-scrobble-enabled t
"Enable/disable last.fm scrobbling.
Decide if the currently playing track should appear in your list
of recently played tracks on last.fm."
:type '(boolean :tag "enabled")
:group 'vuiet)
(defcustom vuiet-automatic-lyrics nil
"Enable/disable the saving of lyrics to the db for all tracks.
If t, download the lyrics for every listened track and save them
to db. This is useful if you're listening to artists and tracks
you already know and like. If nil, the lyrics are only saved
manually, on request, with the `vuiet-playing-track-lyrics'.
This is useful if you're listening to new tracks, some of which
you might not like. Adding the lyrics of such tracks to the db
would only mean adding garbage that you can do without."
:type '(boolean :tag "enabled")
:group 'vuiet)
(defcustom vuiet-update-mode-line-automatically t
"Enable/disable the automatic update of the mode-line.
If enabled, the mode-line is automatically updated after
`vuiet-update-mode-line-interval' seconds. More specifically,
`vuiet-update-mode-line' is called periodically while a track is
playing to update it's current playback position."
:type '(boolean :tag "enabled")
:group 'vuiet)
(defcustom vuiet-update-mode-line-interval 10
"Timeout, in seconds, after which to update the mode-line.
See the `vuiet-update-mode-line-automatically' custom variable
for details."
:type '(number :tag "seconds")
:group 'vuiet)
(defcustom vuiet-artist-similar-limit 15
"Number of artists similar to the given artist.
When considering artists similar to a given artist, take as many
into consideration as this limit. A lower value might mean
artists and tracks you already know and love. A higher value
increases the chances you'll discover something totally new."
:type '(number :tag "count")
:group 'vuiet)
(defcustom vuiet-artist-tracks-limit 15
"Number of tracks for the given artist.
When considering the top tracks for a given artist, take as many
into consideration as this limit. A lower value might mean
tracks from this artist that you already know and love. A higher
value increases the chances you'll discover something totally new
but it also increases the chances that you'll get wrongly
scrobbled songs and youtube will find something totally unrelated
as a result."
:type '(number :tag "count")
:group 'vuiet)
(defcustom vuiet-artist-top-albums-limit 10
"Number of top albums for the given artist.
This value is also used in the artist info page (called by
`vuiet-artist-info') to display the number of top albums."
:type '(number :tag "top-albums-limit")
:group 'vuiet)
(defcustom vuiet-artist-info-show-top-albums nil
"Display the artist top albums in the artist info
buffer (created when calling `vuiet-artist-info'). This adds an
extra call to last.fm which, depending on your system, you might
feel it like an unnecessary lag."
:type '(boolean :tag "info-show-top-albums")
:group 'vuiet)
(defcustom vuiet-tag-artists-limit 15
"Number of artists for the given tag.
When considering the top artists for a given tag, take as many
into consideration as this limit."
:type '(number :tag "count")
:group 'vuiet)
(defcustom vuiet-loved-tracks-limit 500
"Number of tracks to take into consideration when playing user loved tracks.
A number higher than your actual lastfm loved tracks, will take
all of them into consideration. A lower values is useful for
taking into consideration only the most recently loved tracks."
:type '(number :tag "count")
:group 'vuiet)
(defcustom vuiet-youtube-dl-command "yt-dlp"
"The youtube-dl command."
:type '(string :tag "path")
:group 'vuiet)
(cl-defstruct vuiet-track
artist name duration)
(defun vuiet--new-track (artist name &optional duration)
"Prepare the ARTIST and NAME before creating a TRACK object."
(make-vuiet-track :artist (s-trim artist)
:name (s-trim name)
:duration (or duration "")))
(defun vuiet--track-as-string (track)
"Return TRACK as a human-readable string."
(format "%s %s" (vuiet-track-artist track) (vuiet-track-name track)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Utils
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmacro vuiet--artist-from-minibuffer-if-nil (artist)
"If ARTIST is nil, request an artist from the minibuffer.
Use lastfm for autocomplete if, while in the minibuffer, the TAB
key is pressed."
(declare (debug t)
(indent defun))
`(unless ,artist
(let ((keymap (copy-keymap minibuffer-local-map)))
(define-key keymap (kbd "<tab>")
(lambda () (interactive)
(ivy-read "Artist: "
(lastfm-artist-search (minibuffer-contents))
:action (lambda (s)
(setf ,artist (car s))
(delete-minibuffer-contents)
(exit-minibuffer)))))
(let* ((enable-recursive-minibuffers t)
(mini (read-from-minibuffer
"Artist (TAB for completion): " nil keymap)))
(unless (string-empty-p mini)
;; if completion was used, the minibuffer returns an empty string.
(setf ,artist mini))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Browser
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defvar vuiet-mode-map
(let ((keymap (make-sparse-keymap)))
(bind-keys :map keymap
("C-m" . org-open-at-point)
("q" . kill-current-buffer)
("j" . next-line)
("k" . previous-line)
("l" . forward-char)
("h" . backward-char))
keymap))
(define-derived-mode vuiet-mode org-mode "Vuiet")
(defmacro vuiet--with-vuiet-buffer (name &rest body)
"Basic setup for a VUIET-MODE buffer.
Create a new buffer with name NAME if it does not exist. Turn
on VUIET-MODE for that buffer, eval BODY and then switch to it."
(declare (debug t)
(indent defun))
(let ((b (make-symbol "buffer")))
`(aif (get-buffer ,name)
;; Don't create a new buffer if one already exists.
(switch-to-buffer it)
(let ((,b (generate-new-buffer ,name)))
(with-current-buffer ,b
(vuiet-mode)
,@body)
(switch-to-buffer ,b)
(org-previous-visible-heading 1)))))
(defmacro vuiet--local-set-keys (&rest bindings)
"Set multiple key BINDINGS at once.
BINDINGS is a list of (KEY . EXPR) forms. The expansion sets up
a local binding such that KEY executes EXPR."
(declare (debug t)
(indent defun))
`(progn
,@(mapcar (lambda (binding)
`(local-set-key
(kbd ,(car binding))
(lambda () (interactive)
,(cdr binding))))
bindings)))
(defun vuiet-ivy-similar-artists (artist)
"Search similar artists to ARTIST with ivy."
(interactive "sArtist: ")
(ivy-read "Select Artist: "
(lastfm-artist-get-similar
artist
:limit vuiet-artist-similar-limit)
:action (lambda (a)
(vuiet-artist-info (car a)))))
;;;###autoload
(defun vuiet-artist-info (&optional artist)
"Display info about ARTIST in a new buffer.
p play all the artist songs, sequentially.
s select and display info for a similar artist with ivy.
l visit the artist's lastfm page."
(interactive)
(vuiet--artist-from-minibuffer-if-nil artist)
(vuiet--with-vuiet-buffer artist
(let* ((artist-info (lastfm-artist-get-info artist))
(songs (lastfm-artist-get-top-tracks
artist
:limit vuiet-artist-tracks-limit))
(bio-summary (car artist-info))
;; The subseq indices are based on the standard lastfm.el response
;; for artist.info
(similar-artists (cl-subseq artist-info 3 7))
(tags (cl-subseq artist-info 8)))
(insert (format "* %s\n\n %s"
artist
(s-word-wrap 75 (replace-regexp-in-string
"<a.*a>" "" bio-summary))))
(insert "\n\n* Similar artists: \n")
(dolist (artist similar-artists)
(insert (format "|[[elisp:(vuiet-artist-info \"%s\")][%s]]| "
artist artist)))
(insert "\n\n* Popular tags: \n")
(dolist (tag tags)
(insert (format "|[[elisp:(vuiet-tag-info \"%s\")][%s]]| "
tag tag)))
(insert "\n\n* Top Songs: \n")
(cl-loop for i from 1
for track in songs
as song = (s-replace-all '(("[" . "(") ("]" . ")")) (cadr track))
do (insert
(format "%2s. [[elisp:(vuiet-play '((\"%s\" \"%s\")))][%s]]\n"
i artist song song)))
(when vuiet-artist-info-show-top-albums
(insert "\n\n* Top Albums: \n")
(cl-loop for i from 1
for raw-album in (lastfm-artist-get-top-albums
artist :limit vuiet-artist-top-albums-limit)
as album = (s-replace-all '(("(" . "") (")" . "")) (car raw-album))
do (insert
(format "%2s. [[elisp:(vuiet-album-info \"%s\" \"%s\")][%s]]\n"
i artist album album))))
(vuiet--local-set-keys
("p" . (vuiet-play songs))
("s" . (vuiet-ivy-similar-artists artist))
("l" . (vuiet-artist-lastfm-page artist))))))
;;;###autoload
(defun vuiet-artist-info-search (artist)
"Search ARTIST and display info about the selected item.
Similar to `vuiet-artist-info', but search for ARTIST on last.fm
first and then display the info about it."
(interactive "sArtist: ")
(ivy-read "Info for artist: "
(mapcar #'car (lastfm-artist-search artist))
:action #'vuiet-artist-info))
;;;###autoload
(defun vuiet-tag-info (tag)
"Display info about TAG in a new buffer."
(interactive "sTag: ")
(vuiet--with-vuiet-buffer tag
(let ((info (lastfm-tag-get-info tag))
;; (songs (lastfm-tag-get-top-tracks tag :limit 15)) ;; Ignore it
;; (tag-similar (lastfm-tag-get-similar tag)) ;; Empty response
(artists (lastfm-tag-get-top-artists
tag
:limit vuiet-tag-artists-limit)))
(insert (format "* %s\n\n %s \n"
tag
(s-word-wrap 75 (replace-regexp-in-string
"<a.*a>" "" (car info)))))
(insert "\n* Top Artists: \n")
(cl-loop for i from 1
for artist in artists
do (insert
(format "%2s. [[elisp:(vuiet-artist-info \"%s\")][%s]]\n"
i (car artist) (car artist))))
;; Top songs for tags are usually just songs for 2, maximum 3 artists and
;; are usually bullshit and non-relevant for that tag. Skip it.
)))
(defun vuiet--ivy-play-song (songs)
"Choose a song from SONGS with ivy and play it."
(cl-multiple-value-bind (artist-max-len song-max-len)
(cl-loop for entry in songs
maximize (length (car entry)) into artist
maximize (length (cadr entry)) into song
finally (return (cl-values artist song)))
(ivy-read "Play song: "
(mapcar (lambda (song)
(list (format (s-format "%-$0s %-$1s" 'elt
;; Add the padding
`(,artist-max-len ,song-max-len))
;; Add the actual artist and song.
(car song) (cadr song))
(list (car song) (cadr song))))
songs)
:action (lambda (selection)
(vuiet-play (list (cadr selection)))))))
(cl-defun vuiet-loved-tracks-info (&key (page 1) (n 50))
"Display N tracks from the user loved tracks in a new buffer.
If the user has more than N loved tracks, PAGE can be used to show
the next PAGE * N tracks.
<enter> On a song entry, plays that song only.
i Display the next PAGE * N songs.
u Display the previous PAGE * N songs, if N > 1
s Choose a song to play, with ivy."
(interactive)
(vuiet--with-vuiet-buffer "loved-songs"
(let* ((songs (lastfm-user-get-loved-tracks :limit n :page page))
(max-len (cl-loop for entry in songs
maximize (length (car entry)))))
(insert (format "** Loved Songs (Page %s): \n" page))
(cl-loop for i from (+ 1 (* (1- page) n))
for entry in songs
for artist = (car entry)
for song = (cadr entry)
do (insert
(format (concat "%3s. [[elisp:(vuiet-play '((\"%s\" \"%s\")))][%-"
(number-to-string max-len)
"s %s]]\n")
i artist song artist song))
(vuiet--local-set-keys
("i" . (progn (kill-buffer)
(vuiet-loved-tracks-info :page (1+ page) :n n)))
("u" . (when (> page 1)
(kill-buffer)
(vuiet-loved-tracks-info :page (1- page) :n n)))
("s" . (vuiet--ivy-play-song songs)))))))
(defun vuiet-album-info (&optional artist album)
"Display info about the ARTIST's ALBUM in a new buffer.
s choose a song with ivy.
a pick another album with ivy.
p play all songs from the album.
l save lyrics for this album."
(interactive)
(vuiet--artist-from-minibuffer-if-nil artist)
(unless album
(ivy-read (format "%s Album:" artist)
(lastfm-artist-get-top-albums
artist :limit vuiet-artist-top-albums-limit)
:action (lambda (a)
(setf album (car a)))))
(vuiet--with-vuiet-buffer (format "%s - %s" artist album)
(let* ((songs (lastfm-album-get-info artist album))
;; Align song duration in one nice column. For this, I need to know
;; the longest song name from the album.
(max-len (cl-loop for entry in songs
maximize (length (cadr entry)))))
(insert (format "* %s - %s \n\n" artist album))
(cl-loop for i from 1
for entry in songs
for song = (s-replace-all '(("[" . "(") ("]" . ")")) (cadr entry))
for duration = (format-seconds
"%m:%02s" (string-to-number (caddr entry)))
do (insert
(format (concat "%2s. [[elisp:(vuiet-play '((\"%s\" \"%s\")))][%-"
(number-to-string (1+ max-len))
"s]] %s\n")
i artist song song duration)))
(vuiet--local-set-keys
("s" . (vuiet--ivy-play-song songs))
("a" . (vuiet-album-info-search artist)) ;try another album.
("p" . (vuiet-play songs))
("l" . (versuri-save-bulk songs 10))))))
(defun vuiet-album-info-search (artist)
"Search all albums from ARTIST and display the selected one.
The album is displayed in a dedicated buffer. See
`vuiet-album-info' for details regarding the active keybindings
inside this buffer."
(interactive "sArtist: ")
(ivy-read "Select Album: "
(lastfm-artist-get-top-albums
artist :limit vuiet-artist-top-albums-limit)
:action (lambda (album)
(vuiet-album-info artist (car album)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Player
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(let (timer)
(defun vuiet--set-update-mode-line-timer ()
(unless timer
(when vuiet-update-mode-line-automatically
(setf timer
(run-at-time vuiet-update-mode-line-interval
vuiet-update-mode-line-interval
#'vuiet-update-mode-line)))))
(defun vuiet--reset-update-mode-line-timer ()
(when timer
(cancel-timer timer)
(setf timer nil))))
(let (playing-track)
(defun vuiet--playing-track-set (track)
"Update the currently playing track."
(setf playing-track track))
(defun vuiet--playing-track ()
"Return the currently playing track."
playing-track))
(defun vuiet-enable-scrobbling ()
"Enable last.fm scrobbling."
(interactive)
(setf vuiet-scrobble-enabled t))
(defun vuiet-disable-scrobbling ()
"Disable last.fm scrobbling."
(interactive)
(setf vuiet-scrobble-enabled nil))
(defun vuiet-enable-automatic-lyrics ()
"Enable saving the lyrics for all listened tracks to the db.
See `vuiet-automatic-lyrics' for details."
(interactive)
(setf vuiet-automatic-lyrics t)
(message "Automatic lyrics for vuiet, enabled"))
(defun vuiet-disable-automatic-lyrics ()
"Enable saving the lyrics for all listened tracks to the db.
See `vuiet-automatic-lyrics' for details."
(interactive)
(setf vuiet-automatic-lyrics nil)
(message "Automatic lyrics for vuiet, disabled"))
(defun vuiet-toggle-automatic-lyrics ()
"Toggle saving the lyrics for all listened tracks to the db.
See `vuiet-automatic-lyrics' for details."
(interactive)
(setf vuiet-automatic-lyrics (not vuiet-automatic-lyrics))
(message (format "Automatic lyrics for vuiet, %s"
(if vuiet-automatic-lyrics "enabled" "disabled"))))
(defun vuiet-update-mode-line (&optional position)
"Update the mode-line."
(interactive)
(let ((track (vuiet--playing-track)))
(when track
(setq-default mode-line-misc-info
(list (format "%s - %s [%s/%s] "
(vuiet-track-artist track)
(vuiet-track-name track)
(format-time-string "%M:%S"
(or position
;; At startup, the running track may be set, by the
;; file might not be loaded yet.
(condition-case nil
(mpv-get-playback-position)
(error 0))))
(vuiet-track-duration track))))))
(force-mode-line-update t))
(defun vuiet-stop ()
"Stop playing and clear the mode line."
(interactive)
(vuiet--playing-track-set nil)
(setf mpv-on-exit-hook nil)
(mpv-kill)
(vuiet--reset-update-mode-line-timer)
(setq-default mode-line-misc-info nil))
(defun vuiet-playing-artist ()
"Return the currently playing artist."
(awhen (vuiet--playing-track)
(vuiet-track-artist it)))
(defun vuiet-playing-track-name ()
"Return the currently playing track name."
(awhen (vuiet--playing-track)
(vuiet-track-name it)))
(defun vuiet-playing-track-str ()
"Return the playing TRACK as a human-readable string."
(awhen (vuiet--playing-track)
(vuiet--track-as-string it)))
(defun vuiet-next ()
"Skip the currently playing track and play the next."
(interactive)
(condition-case nil
(mpv-run-command "playlist-next")
(error (display-message-or-buffer "No track available; Try again"))))
(defun vuiet-peek-next ()
"Display the next track in the mode-line for a few seconds."
(interactive)
(let ((urls (vuiet--mpv-playlist-remaining-urls)))
(when (> (length urls) 1)
(let ((track (vuiet--track-from-youtube-url
(cadr (vuiet--mpv-playlist-remaining-urls)))))
(setq-default mode-line-misc-info
(list (format "%s - %s (%s) "
(vuiet-track-artist track)
(vuiet-track-name track)
(vuiet-track-duration track))))
(run-at-time 3 nil #'vuiet-update-mode-line)))))
(defun vuiet-previous ()
"Replay the previous track.
It only considers tracks from the current playlist."
(interactive)
(condition-case nil
(mpv-run-command "playlist-prev")
(error (display-message-or-buffer "This is the first track"))))
(defun vuiet-replay ()
"Play the currently playing track from the beginning."
(interactive)
(mpv-seek 1)
(vuiet-update-mode-line))
(defun vuiet-seek-backward (&optional arg)
"Seek backward ARG seconds. ARG defaults to 5."
(interactive "P")
(mpv-seek-backward (or arg 5))
(vuiet-update-mode-line))
(defun vuiet-seek-forward (&optional arg)
"Seek forward ARG seconds. ARG defaults to 5."
(interactive "P")
(mpv-seek-forward (or arg 5))
(vuiet-update-mode-line))
(defun vuiet-seek-backward-rate (&optional arg)
"Seek backward ARG percents of the track. ARG defaults to 10."
(interactive "P")
(mpv-run-command "seek" (- (or arg 10)) "relative-percent")
(vuiet-update-mode-line))
(defun vuiet-seek-forward-rate (&optional arg)
"Seek forward ARG percents of the track. ARG defaults to 10."
(interactive "P")
(mpv-run-command "seek" (or arg 10) "relative-percent")
(vuiet-update-mode-line))
(defun vuiet-play-pause ()
"Toggle the play/pause status."
(interactive)
(mpv-pause)
(vuiet-update-mode-line))
(defun vuiet-player-volume ()
"Get the music player volume, between 0% and 100%."
(mpv-get-property "volume"))
(defun vuiet-player-volume-inc (&optional arg)
"Increase the music player volume by ARG percent. ARG defaults to 10."
(interactive "P")
(let* ((volume (vuiet-player-volume))
(new-volume (+ volume (or arg 10))))
(when (<= new-volume 100)
(mpv-set-property "volume" new-volume))))
(defun vuiet-player-volume-dec (&optional arg)
"Decrease the mpv player volume by ARG percent. ARG defaults to 10."
(interactive "P")
(let* ((volume (vuiet-player-volume))
(new-volume (- volume (or arg 10))))
(when (>= new-volume 0)
(mpv-set-property "volume" new-volume))))
(defun vuiet-playing-artist-info ()
"Display info for the currently playing artist in a new buffer."
(interactive)
(awhen (vuiet-playing-artist)
(vuiet-artist-info it)))
(defun vuiet-playing-track-search-youtube ()
"Open a youtube search for the currently playing track."
(interactive)
(awhen (vuiet-playing-track-str)
(browse-url
(format "https://www.youtube.com/results?search_query=%s" it))))
(defun vuiet--youtube-link-at-position ()
"Return youtube link of the current track at the playback position."
(concat "https://www.youtube.com/"
(mpv-get-property "filename")
"&t=" (int-to-string
(round (mpv-get-playback-position)))))
(defun vuiet-playing-track-continue-on-youtube ()
"Pause vuiet and continue playing on youtube."
(interactive)
(vuiet-play-pause)
(when (vuiet-playing-artist)
(browse-url
(vuiet--youtube-link-at-position))))
(defun vuiet-playing-track-continue-with-mpv ()
"Pause vuiet and continue playing with mpv as a new process."
(interactive)
(vuiet-play-pause)
(when (vuiet-playing-artist)
(start-process "vuiet" nil "mpv"
(vuiet--youtube-link-at-position))))
(defun vuiet-artist-lastfm-page (artist)
"Visit the ARTIST lastfm page."
(interactive "sArtist: ")
(browse-url
(format "https://last.fm/music/%s"
(s-replace " " "+" artist))))
(defun vuiet-playing-artist-lastfm-page ()
"Visit he currently playing artist lastfm page."
(interactive)
(awhen (vuiet-playing-artist)
(vuiet-artist-lastfm-page it)))
(defun vuiet-love-track ()
"Add the currently playing track to the user loved songs."
(interactive)
(when (vuiet--playing-track)
(lastfm-track-love (vuiet-playing-artist)
(vuiet-playing-track-name))))
(defun vuiet-unlove-track ()
"Remove the currently playing track from the user loved songs."
(interactive)
(when (vuiet--playing-track)
(lastfm-track-unlove (vuiet-playing-artist)
(vuiet-playing-track-name))))
(defun vuiet-playing-track-lyrics ()
"Display the lyrics for the currently playing track in a new buffer.
See `versuri-display' for the active keybindings inside this buffer."
(interactive)
(when (vuiet--playing-track)
(versuri-display (vuiet-playing-artist)
(vuiet-playing-track-name))))
(defun vuiet--scrobble-track (track)
"Scrobble TRACK on lastfm, if it's the same as the playing track."
(when (equal track (vuiet--playing-track))
(let ((timestamp (round (time-to-seconds (current-time)))))
(lastfm-track-scrobble (vuiet-track-artist track)
(vuiet-track-name track)
(int-to-string timestamp)))))
(defun vuiet--next-track (tracks)
"Yield the next VUIET-TRACK object from the TRACKS list.
If no more objects available, return nil."
(condition-case nil
(iter-next tracks)
(iter-end-of-sequence nil)))
;;;:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Playlists
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(iter-defun vuiet--make-generator (songs random)
"Make a generator of VUIET-TRACK objects from the SONGS list.
If RANDOM is true, each call to the generator will yield a random
song and the generator is infinite. Otherwise, the generator
will yield each of the (length songs) elements, sequentially."
(while songs
(let ((song (if random
(seq-random-elt songs)
(prog1 (car songs)
(setf songs (cdr songs))))))
(iter-yield
(vuiet--new-track (car song) (cadr song))))))
;; Keep a hash table of youtube track ids and vuiet-tracks
(let ((id-tracks (make-hash-table :test 'equal)))
(defun vuiet--add-id-track (id track)
(puthash id track id-tracks))
(defun vuiet--get-track-from-id (id)
(gethash id id-tracks)))
(defun vuiet--mpv-playlist-remaining-urls ()
"Return the remaining mpv urls, including this one."
(let ((pos (mpv-get-property "playlist-pos"))
(count (mpv-get-property "playlist-count")))
(cl-loop for c from pos to (- count 1)
collect (mpv-get-property
(format "playlist/%s/filename" c)))))
(defun vuiet--mpv-playlist-remaining-urls-count ()
(- (mpv-get-property "playlist-count")
(mpv-get-property "playlist-pos-1")))
(defun vuiet--track-from-youtube-url (url)
"Return the track object associated with the youtube id."
(vuiet--get-track-from-id
(cadr (s-split "=" url))))
(defun vuiet--mpv-playing-track ()
"Return the currently mpv playing track."
(vuiet--track-from-youtube-url
(mpv-get-property "filename")))
(defun vuiet--mpv-add-track (generator &optional keep-old)
"Append the next track url from GENERATOR to mpv.
If the GENERATOR is not empty, get the next track from it, find
it's youtube url and append it to the currently running mpv
instance playlist. When the url is played in the future, I want
to also have a means to recover the track name from it, thus, I
am saving the track for the given youtube url in a hash table."
(let ((track (vuiet--next-track generator)))
(when track
(set-process-filter
(start-process "ytdl" nil vuiet-youtube-dl-command
(format "ytsearch:%s" (vuiet--track-as-string track))
"--get-id"
"--get-duration")
(lambda (_ id-duration-str)
(let* ((id-duration (s-split "\n" id-duration-str))
(id (car id-duration))
(url (concat "https://www.youtube.com/watch?v=" id))
(duration (cadr id-duration)))
(setf (vuiet-track-duration track) duration)
(vuiet--add-id-track id track)
(if keep-old
(mpv-run-command "loadfile" url "append-play")
(mpv-run-command "loadfile" url "replace"))))))))
(let (gen)
(defun vuiet--play (generator)
"Start mpv and play the tracks from the GENERATOR."
(unless (mpv-live-p)
(mpv-start "--no-video"
"--idle=yes"
"--keep-open=yes")
(setf mpv-on-event-hook
(lambda (event)
(pcase (cdr (car event))
("playback-restart"
(mpv-set-property "pause" "no"))
("start-file"
;; Buffer some tracks beforehand, if needed.
(when (< (vuiet--mpv-playlist-remaining-urls-count) 1)
(vuiet--mpv-add-track gen t)))
("file-loaded"
(let ((track (vuiet--mpv-playing-track)))
(vuiet--playing-track-set track)
(vuiet-update-mode-line 0)
(mpv-set-property "pause" "no")
(when vuiet-scrobble-enabled
(run-at-time vuiet-scrobble-timeout nil
#'vuiet--scrobble-track track))
(when vuiet-automatic-lyrics
(versuri-lyrics (vuiet-playing-artist)
(vuiet-playing-track-name)
(lambda (lyrics) "do nothing")))))))))
;; If the player was already started, change the generator and clear the
;; mpv playlist but leave the hooks and everything else in place.
(setf gen generator)
(if current-prefix-arg
;; Calling any play command with prefix arg, postpones the changing of
;; playlists until the current track ends.
(progn
(setf current-prefix-arg nil)
;; Keep only the currently playing track in the playlist.
(mpv-run-command "playlist-clear")
(vuiet--mpv-add-track generator t))
(mpv-set-property "pause" "yes")
(vuiet--mpv-add-track generator nil))
(vuiet--set-update-mode-line-timer)))
;;;###autoload
(cl-defun vuiet-play (songs &key (random nil))
"Play everyting in the SONGS list, randomly or sequentially.
SONGS is a list of type ((artist1 song1) (artist2 song2) ...)."
(vuiet--play
(vuiet--make-generator songs random)))
;;;###autoload
(defun vuiet-play-artist (&optional artist)
"Play the ARTIST top tracks, RANDOM or sequentially."
(interactive)
(vuiet--artist-from-minibuffer-if-nil artist)
(vuiet-play (lastfm-artist-get-top-tracks
artist
:limit vuiet-artist-tracks-limit)
:random (y-or-n-p (format "%s: Play random? " artist))))
;;;###autoload
(defun vuiet-play-playing-artist ()
"Play the currently playing artist's top tracks."
(interactive)
(when (vuiet--playing-track)
(vuiet-play-artist (vuiet-playing-artist))))
(defun vuiet-play-playing-track-album ()
"Play the full album of the currently playing track."
(interactive)
(when (vuiet--playing-track)
(let* ((artist (vuiet-playing-artist))
(song (vuiet-playing-track-name))
(album (car (lastfm-track-get-info artist song))))
(vuiet-play (lastfm-album-get-info artist album)))))
(defun vuiet-info-playing-track-album ()
"Open an info buffer for the currently playing track album."
(interactive)
(when (vuiet--playing-track)
(let* ((artist (vuiet-playing-artist))
(song (vuiet-playing-track-name))
(album (car (lastfm-track-get-info artist song))))
(vuiet-album-info artist album))))
;;;###autoload
(defun vuiet-play-album (&optional artist album)
"Play the whole ALBUM of the given ARTIST.
If called interactively, the album can be picked interactively
from the ARTIST's top albums, where ARTIST is given in the
minibuffer."
(interactive)
(if (and artist album)
(vuiet-play (lastfm-album-get-info artist album))
(vuiet--artist-from-minibuffer-if-nil artist)
(ivy-read (format "Play %s Album" artist)
(lastfm-artist-get-top-albums
artist :limit vuiet-artist-top-albums-limit)
:action (lambda (album)
(vuiet-play
(lastfm-album-get-info artist (car album)))))))
(iter-defun vuiet--artists-similar-tracks (artists)
"Return a generator of tracks based on the given ARTISTS.
The generator yields top tracks from artists similar to the given
artist or the given list of artists."
(while t
(let* ((artists (lastfm-artist-get-similar
(seq-random-elt artists)
:limit vuiet-artist-similar-limit))
(artist (car (seq-random-elt artists)))
(track (cadr (seq-random-elt
(lastfm-artist-get-top-tracks
artist
:limit vuiet-artist-tracks-limit)))))
(iter-yield (vuiet--new-track artist track)))))
;;;###autoload
(defun vuiet-play-artist-similar (&optional artists)
"Play tracks from artists similar to ARTISTS.
ARTISTS is a list of strings of the form '(artist1 artist2 etc.)
If called interactively, multiple artists can be provided in the
minibuffer if they are sepparated by commas."
(interactive)
(vuiet--artist-from-minibuffer-if-nil artists)
(vuiet--play
(vuiet--artists-similar-tracks
(if (stringp artists)
;; The function was called interactively.
(mapcar #'s-trim (s-split "," artists))
artists))))
(defun vuiet-play-playing-artist-similar ()
"Play tracks from artists similar to the playing artist.
This function is similar to `vuiet-play-artist-similar', only the
list of artists is limited to the artist of the currently playing
track."
(interactive)
(vuiet-play-artist-similar (vuiet-playing-artist)))
(iter-defun vuiet--tags-similar-tracks (tags)
"Return a generator of tracks based on the given TAGS.
Return a random track from a random artist for a random tag in
the list of TAGS."
(while t
(let* ((artists (lastfm-tag-get-top-artists
(seq-random-elt tags)
:limit vuiet-tag-artists-limit))
(artist (car (seq-random-elt artists)))
(track (cadr (seq-random-elt
(lastfm-artist-get-top-tracks
artist
:limit vuiet-artist-tracks-limit)))))
(iter-yield (vuiet--new-track artist track)))))
;;;###autoload
(defun vuiet-play-tag-similar (tags)
"Play tracks from artists similar to TAGS.
TAGS is a list of strings of the form '(tag1 tag2 etc.)
If called interactively, multiple tags can be provided in the
minibuffer if they are sepparated by commas."
(interactive "sTag(s): ")
(vuiet--play
(vuiet--tags-similar-tracks
(if (stringp tags)
;; The function was called interactively.
(mapcar #'s-trim (s-split "," tags))
tags))))
(defun vuiet-play-playing-tags-similar ()
"Play tracks from artists with similar tags as the current tags.
Play tracks from random artists that have tags equal to one of
the tags of the currently playing artist."
(interactive)
(vuiet--play
(vuiet--tags-similar-tracks
(mapcar #'car (lastfm-artist-get-top-tags
(vuiet-playing-artist))))))
;;;###autoload
(defun vuiet-play-track (&optional artist name)
"Play the song NAME from the given ARTIST.
If called interactively, let the user select and play one of the
ARTIST's top songs, where ARTIST is given in the minibuffer."
(interactive)
(if (and artist name)
(vuiet-play `((,artist ,name)))
(vuiet--artist-from-minibuffer-if-nil artist)
(vuiet--ivy-play-song
(lastfm-artist-get-top-tracks artist
:limit vuiet-artist-tracks-limit))) )
;;;###autoload
(defun vuiet-play-track-search (track)
"Search TRACK and play the selected item.
Similar to `vuiet-play-track', but search for TRACK on last.fm
first and then let the user select one of the results."
(interactive "sTrack: ")
(alet (lastfm-track-search track)
(vuiet--ivy-play-song
;; Transform into a list if last.fm returned only one result.
(if (listp (car it)) it (list it)))))
;;;###autoload
(defun vuiet-play-track-by-lyrics (lyrics)
"Search a track by LYRICS and play it."
(interactive "sLyrics: ")
(let ((track (versuri-ivy-search lyrics)))
(vuiet-play (list track))))
;;;###autoload
(defun vuiet-play-loved-track ()
"Select a track from the user loved tracks and play it.
The user loved tracks list is the one associated with the