Browse code
Initial
fiddlerwoaroof authored on 13/09/2016 06:37:20
Showing 6 changed files
Showing 6 changed files
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))) |