music.lisp 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. (in-package :cl-user)
  2. (defpackage chatikbot.plugins.music
  3. (:use :cl :chatikbot.common :alexandria))
  4. (in-package :chatikbot.plugins.music)
  5. (defvar *cookies* (cl-cookie:make-cookie-jar))
  6. (defvar *deluge-api* "http://localhost:8112/json")
  7. (defvar *deluge-password* "chads")
  8. (defvar *deluge-request-id* 1)
  9. (defun jojo-request (url &rest args &key method parameters content headers basic-auth cookie-jar keep-alive use-connection-pool timeout ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path user-agent (as :plist))
  10. (declare (ignore method parameters basic-auth cookie-jar keep-alive use-connection-pool timeout ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path user-agent))
  11. (remf args :as)
  12. (when content
  13. (push (cons :content-type "application/json") headers))
  14. (remf args :headers)
  15. (multiple-value-bind (body status headers uri)
  16. (apply #'http-request url :headers headers args)
  17. (unless (stringp body)
  18. (setf body (trivial-utf-8:utf-8-bytes-to-string body)))
  19. (values (jojo:parse body :as as) status headers uri)))
  20. (defun deluge-request (method &optional params is-auth)
  21. (let* ((content (trivial-utf-8:string-to-utf-8-bytes
  22. (jojo:to-json `(:|id| ,(incf *deluge-request-id*)
  23. :|method| ,method
  24. :|params| ,params))))
  25. (response (jojo-request *deluge-api* :method :post
  26. :content content
  27. :cookie-jar *cookies*))
  28. (error-message (getf (getf response :|error|) :|message|)))
  29. (if error-message
  30. (if (and (null is-auth)
  31. (equal error-message "Not authenticated"))
  32. (progn
  33. (deluge-auth)
  34. (deluge-request method params t)) ;; Retry original request
  35. (error error-message))
  36. (getf response :|result|))))
  37. (defun deluge-auth ()
  38. (deluge-request "auth.login" (list *deluge-password*) t))
  39. (defun infohash-magnet (info-hash)
  40. (concatenate 'string "magnet:?xt=urn:btih:" info-hash))
  41. (defun deluge-add-info-hash (info-hashes)
  42. (unless (listp info-hashes)
  43. (setf info-hashes (list info-hashes)))
  44. (let ((torrents (loop for ih in info-hashes
  45. collect (list :|path| (infohash-magnet ih)
  46. :|options| '(:|add_paused| :true)))))
  47. (deluge-request "web.add_torrents" (list torrents))))
  48. (defun deluge-get-torrents ()
  49. (deluge-request "core.get_session_state"))
  50. (defun deluge-get-torrent-status (info-hash fields)
  51. (deluge-request "core.get_torrent_status" (list info-hash fields)))
  52. (defun deluge-get-torrents-status (filter fields)
  53. (deluge-request "core.get_torrents_status" (list filter fields)))
  54. (defun deluge-remove-torrent (ih &optional from-disk)
  55. (deluge-request "core.remove_torrent" (list ih from-disk)))
  56. (defparameter +deluge-default-status-fields+ '("name" "state" "save_path" "files" "file_priorities" "total_size" "total_wanted" "total_done" "num_files"))
  57. (defun raw-pathname (pathspec)
  58. (values (cl-ppcre:regex-replace-all "\\[" pathspec "\\\\[")))
  59. (defun deluge-get-torrent-files (info-hash)
  60. (let* ((status (deluge-request "core.get_torrent_status" (list info-hash +deluge-default-status-fields+)))
  61. (save-path (getf status :|save_path|)))
  62. (loop for file in (getf status :|files|)
  63. for prio in (getf status :|file_priorities|)
  64. do (setf (getf file :|prio|) prio
  65. (getf file :|path|) (pathname (raw-pathname (format nil "~A/~A" save-path (getf file :|path|)))))
  66. collect file)))
  67. (defun get-non-skipped (files)
  68. (remove 0 files :key (lambda (f) (getf f :|prio|))))
  69. (defun get-files-folders (files)
  70. (delete-duplicates (mapcar (lambda (f) (uiop:pathname-directory-pathname (getf f :|path|))) files)))
  71. (defparameter +media-exts+ '("mp3" "flac" "ac3"))
  72. (defun media-p (path)
  73. (let* ((ext (subseq path (1+ (or (position #\. path :from-end t) (1- (length path)))))))
  74. (member (string-downcase ext) +media-exts+ :test #'equal)))
  75. (defun get-torrent-info (status)
  76. (labels ((f (field)
  77. (getf status field)))
  78. (let ((wanted-dirs (loop for file in (f :|files|)
  79. for prio in (f :|file_priorities|)
  80. for path = (getf file :|path|)
  81. when (media-p path)
  82. unless (= prio 0)
  83. collect (subseq path 0 (1+ (position #\/ path :from-end t))))))
  84. (list
  85. :name (f :|name|)
  86. :state (keyify (f :|state|))
  87. :save-path (f :|save_path|)
  88. :total-size (f :|total_size|)
  89. :total-wanted (f :|total_wanted|)
  90. :total-done (f :|total_done|)
  91. :num-files (f :|num_files|)
  92. :wanted-files (length wanted-dirs)
  93. :media-dirs (remove-duplicates wanted-dirs :test #'equal)))))
  94. (defun get-import-notify (info)
  95. (labels ((f (fl) (getf info fl)))
  96. (format nil "[[🎶]] Importing *~a*, ~a tracks in ~a albums. Total size ~a"
  97. (f :name) (f :wanted-files) (length (f :media-dirs)) (format-size (f :total-wanted)))))
  98. (defun get-root-paths (info)
  99. (let ((roots (delete-duplicates (loop for dir in (getf info :media-dirs)
  100. collect (subseq dir 0 (1+ (position #\/ dir))))
  101. :test #'equal))
  102. (save (getf info :save-path)))
  103. (mapcar #'(lambda (r) (concatenate 'string save "/" r)) roots)))
  104. (defvar *beet-bin* '("sudo" "-u" "uploader" "/home/uploader/.local/bin/beet"))
  105. (defun beets-import-cmd (paths)
  106. (append *beet-bin*
  107. (list "import" "--move" "--quiet")
  108. paths))
  109. (defun run-import (paths &optional dry)
  110. (let ((cmd (beets-import-cmd paths)))
  111. (if dry (format t "Running ~{~a~^ ~}" cmd)
  112. (uiop:run-program cmd))))
  113. (defun deluge-get-seeding-torrents ()
  114. (loop for (torrent state) on (deluge-get-torrents-status '(:|state| "Seeding") '("name" "total_wanted")) by #'cddr
  115. collect (list (string torrent) (getf state :|name|) (getf state :|total_wanted|)))
  116. (defun deluge-pause-torrents (&rest torrents)
  117. (deluge-request "core.pause_torrent" (list torrents)))
  118. (defun deluge-delete-skipped (torrent &optional dry)
  119. (let* ((status (deluge-request "core.get_torrent_status" (list torrent +deluge-default-status-fields+)))
  120. (save-path (getf status :|save_path|)))
  121. (loop for file in (getf status :|files|)
  122. for prio in (getf status :|file_priorities|)
  123. for path = (pathname (raw-pathname (format nil "~A/~A" save-path (getf file :|path|))))
  124. when (zerop prio)
  125. when (probe-file path)
  126. do (if dry (format t "Deleting '~A'~%" path)
  127. (uiop:delete-file-if-exists path)))))
  128. (defun send-admins (msg &optional dry)
  129. (if dry (format t "~a~%" msg)
  130. (loop for *chat-id* in (lists-get :music-admins)
  131. do (bot-send-message msg :parse-mode "markdown"))))
  132. (defun process-downloaded (ih &optional dry)
  133. (let* ((status (deluge-get-torrent-status ih +deluge-default-status-fields+))
  134. (info (get-torrent-info status))
  135. (notify (get-import-notify info))
  136. (paths (get-root-paths info)))
  137. (send-admins notify dry)
  138. (deluge-pause-torrents ih)
  139. (deluge-delete-skipped ih dry)
  140. (run-import paths dry)
  141. (if dry (format t "Removing torrent ~a" ih)
  142. (deluge-remove-torrent ih))))
  143. (defun process-imports ()
  144. (loop
  145. (loop for (ih name size) in (deluge-get-seeding-torrents)
  146. do (handler-case (process-downloaded ih)
  147. (error (e) (log:error "Error processing torrent" name e))))
  148. (sleep 1)))
  149. (defvar *watcher* nil "Importer thread ")
  150. (defvar *lock* (bordeaux-threads:make-recursive-lock "Music watch"))
  151. (defun ensure-watcher ()
  152. (bt:with-recursive-lock-held (*lock*)
  153. (if (lists-get :music-watch)
  154. (unless *watcher*
  155. (setf *watcher* (bt:make-thread 'process-imports :name "music watch")))
  156. (when *watcher*
  157. (bt:destroy-thread *watcher*)
  158. (setf *watcher* nil))))
  159. (values))
  160. (defun handle-set-watch (enable)
  161. (lists-set-entry :music-watch *chat-id* enable)
  162. (ensure-watcher)
  163. (bot-send-message (if enable "[[🎶]] Импортим музло" "[[🎶]] Пусть мешки парятся")))
  164. (defun handle-status ())
  165. (def-message-cmd-handler handle-cmd-music (:music)
  166. (cond
  167. ((= 1 (length *args*))
  168. (handle-set-watch (equal "on" (car *args*))))
  169. (:otherwise (handle-status))))
  170. (add-hook :starting #'ensure-watcher)