git.fiddlerwoaroof.com
Browse code

Initial

fiddlerwoaroof authored on 13/09/2016 06:37:20
Showing 6 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,3 @@
1
+*\~
2
+\#*\#
3
+'
0 4
new file mode 100644
... ...
@@ -0,0 +1 @@
1
+This is the stub README.txt for the "mpd-remote" project.
0 2
new file mode 100644
... ...
@@ -0,0 +1,17 @@
1
+;;;; mpd-remote.asd
2
+
3
+(asdf:defsystem #:mpd-remote
4
+  :description "Describe mpd-remote here"
5
+  :author "Your Name <your.name@example.com>"
6
+  :license "Specify license here"
7
+  :depends-on (#:fwoar.lisputils
8
+               #:alexandria
9
+	       #:serapeum
10
+	       #:flexi-streams
11
+	       #:usocket)
12
+               
13
+  :serial t
14
+  :components ((:file "package")
15
+               (:file "song-handling")
16
+               (:file "mpd-remote")))
17
+
0 18
new file mode 100644
... ...
@@ -0,0 +1,86 @@
1
+;;;; mpd-remote.lisp
2
+
3
+(in-package #:mpd-remote)
4
+
5
+;;; "mpd-remote" goes here. Hacks and glory await!
6
+
7
+(defun call-with-mpd-connection (host port cb)
8
+  (usocket:with-client-socket (sock stream host port :element-type '(unsigned-byte 8))
9
+    (let ((stream (flexi-streams:make-flexi-stream stream :external-format :utf8)))
10
+      (assert (alexandria:starts-with-subseq "OK" (read-line stream)))
11
+      (funcall cb sock stream))))
12
+
13
+(define-condition mpd-error ()
14
+  ((message :initarg :message :reader message))
15
+  (:report (lambda (condition stream)
16
+	     (format stream "~a" (message condition)))))
17
+
18
+(defmacro do-until ((end-condition &optional result) &body body)
19
+  `(do () (,end-condition ,@(when result `(,result)))
20
+     ,@body))
21
+
22
+(defun get-response (stream)
23
+  (let ((result (list))
24
+	(done nil))
25
+      (do-until (done (nreverse result))
26
+	(let ((line (read-line stream)))
27
+	  (format t "~&~s~%" line)
28
+	  (if (string= line "OK")
29
+	      (setf done t)
30
+	      (multiple-value-bind (error-p suffix) (alexandria:starts-with-subseq "ACK" line :return-suffix t)
31
+		(if error-p
32
+		    (error 'mpd-error :message (serapeum:trim-whitespace suffix))
33
+		    (push line result))))))))
34
+
35
+(defmacro with-mpd-connection ((socket-sym stream-sym &optional (host "127.0.0.1") (port 6600)) &body body)
36
+  `(call-with-mpd-connection ,host ,port
37
+			     (lambda (,socket-sym ,stream-sym)
38
+			       ,@body)))
39
+
40
+(defclass mpd-command ()
41
+  ())
42
+
43
+(defgeneric render-mpd-command (command args))
44
+
45
+(defgeneric send-command (stream command &key)
46
+  (:method (stream command &rest r &key)
47
+    (write-line (render-mpd-command command
48
+				    r)
49
+		stream)
50
+    (finish-output stream)
51
+    (get-response stream)))
52
+
53
+(defclass current-song (mpd-command)
54
+  ())
55
+
56
+(defmacro mpd-progn ((stream) &body body)
57
+  `(let ((*standard-output* ,stream))
58
+     (write-line "command_list_ok_begin")
59
+     (unwind-protect (progn ,@body)
60
+       (write-line "command_list_end"))))
61
+
62
+(defmethod render-mpd-command ((command (eql :current-song)) args)
63
+  (declare (ignore command args))
64
+  "currentsong")
65
+
66
+(defmethod send-command :around (stream (command (eql :current-song)) &key)
67
+  (declare (ignore stream))
68
+  (mpd-remote.song:parse-song (call-next-method)))
69
+
70
+(defmethod render-mpd-command ((command (eql :playlist-info)) args)
71
+  (declare (ignore command))
72
+  (format nil "playlistinfo ~{~a~^:~}" (getf args :slice)))
73
+
74
+(defmethod send-command :around (stream (command (eql :playlist-info)) &key slice)
75
+  (declare (ignore command stream))
76
+  (mapcar #'mpd-remote.song:parse-song
77
+	  (mpd-remote.song:split-songs (call-next-method))))
78
+
79
+(defmethod render-mpd-command ((command (eql :play)) args)
80
+  (declare (ignore command))
81
+  "play")
82
+
83
+(defmethod render-mpd-command ((command symbol) args)
84
+  (declare (ignore args))
85
+  (when (keywordp command)
86
+    (string-downcase command)))
0 87
new file mode 100644
... ...
@@ -0,0 +1,10 @@
1
+;;;; package.lisp
2
+
3
+(defpackage #:mpd-remote.song
4
+  (:use #:cl #:alexandria #:serapeum)
5
+  (:export #:parse-song #:split-songs))
6
+
7
+(defpackage #:mpd-remote
8
+  (:use #:cl #:alexandria #:serapeum #:mpd-remote.song)
9
+  (:export #:with-mpd-connection #:send-command))
10
+
0 11
new file mode 100644
... ...
@@ -0,0 +1,67 @@
1
+(in-package #:mpd-remote.song)
2
+
3
+(defclass song ()
4
+  ((tags :accessor tags :initform (make-hash-table :test 'equalp))))
5
+
6
+(defmacro define-tag-accessor (name class &optional (tag name))
7
+  `(defgeneric ,name (song)
8
+     (:method ((song ,class))
9
+       (gethash ,(string tag)
10
+		(tags song)))))
11
+
12
+(defmacro define-tag-accessors ((class) &body tags)
13
+  `(progn
14
+     ,@(loop for (name tag) in (fw.lu:ensure-mapping tags)
15
+	  collect `(define-tag-accessor ,name ,class,tag))))
16
+
17
+
18
+
19
+(define-tag-accessors (song)
20
+  file artist album title genre last-modified track date (track-length time) pos)
21
+
22
+(defmethod print-object ((song song) stream)
23
+  (print-unreadable-object (song stream :type t :identity t)
24
+    (with-accessors ((tags tags)) song
25
+      (format stream "(~a - ~a)"
26
+	      (gethash "title" tags)
27
+	      (gethash "artist" tags)))))
28
+
29
+(defun parse-tag (tag value)
30
+  (string-case:string-case ((string-downcase tag) :default value)
31
+    ("track" (mapcar #'parse-integer
32
+		     (split-sequence #\/ value)))
33
+    ("disc" (mapcar #'parse-integer
34
+		    (split-sequence #\/ value)))
35
+    ("time" (parse-integer value))  
36
+    ("pos" (parse-integer value))
37
+    ("date" (parse-integer value))))
38
+
39
+(defun parse-song (lines)
40
+  (let ((song (make-instance 'song)))
41
+    (dolist (line lines song)
42
+      (destructuring-bind (tag value) (split-sequence #\: line :count 2)
43
+	(setf (gethash tag (tags song))
44
+	      (parse-tag tag (trim-whitespace value)))))))
45
+
46
+(defun split-songs (lines)
47
+  (let* ((result (list (list))))
48
+    (dolist (line lines (mapcar #'nreverse (cdr (nreverse result))))
49
+      (when (starts-with-subseq "file:" line)
50
+	(push (list) result))
51
+      (push line (car result)))))
52
+				
53
+	 
54
+(defun parse-search (lines)
55
+  (declare (optimize (debug 3)))
56
+  (loop with result = (make-array 0 :adjustable t :fill-pointer 0)
57
+     with current = (list)
58
+     for line in lines
59
+     for (tag value) = (split-sequence #\: line)
60
+
61
+     when (string-equal tag "file") do
62
+       (vector-push-extend current result)
63
+       (setf current (list))
64
+
65
+     do (push (vector tag (string-trim '(#\space #\tab) value))
66
+	      current)
67
+     finally (return result)))