;;;; model.lisp (in-package :vampire) ;;; CLASSES (defclass/bknr keyed () ((key :r :std (nuid) :index-type string-unique-index :index-reader object-with-key))) (defclass/bknr user (keyed) ((name :with :index-type string-unique-index :index-reader user-with-name :index-initargs (:test 'equalp)) (playlists :with :std (list)) (pwsalt :with :std (nuid)) (pwhash :with))) (defclass/bknr invite (keyed) ((maker :ri :std nil :index-type hash-index :index-reader invites-by-maker) (uses-remaining :std nil))) (defclass/bknr playlist (keyed) ((title :with :std (default-name "playlist")) (tracks editors :with :std (list)) (cover-image :with :std nil :doc "A url to the cover of this album.") (user :with :std (error "A USER is required to have created the content.")))) (defmethod initialize-instance :after ((pl playlist) &key) (pushnew pl (user-playlists (playlist-user pl)) :test #'eq)) (defclass/bknr track (keyed) ((source file title artist album thumb-url duration codec :with) (playlists :with :std (list) :doc "A list of playlists in which this track appears"))) ;;; MODEL OPERATIONS (defun can-edit-p (user playlist) (or (eq (playlist-user playlist) user) (member user (playlist-editors playlist) :test #'eq))) (defun invite-by-code (code) "Returns NIL if CODE is an invalid invite code. Returns the INVITE instance otherwise" (when-let (obj (object-with-key code)) (and (typep obj 'invite) (or (null (uses-remaining obj)) (plusp (uses-remaining obj))) obj))) (defun user-with-key (key) (when-let (obj (object-with-key key)) (when (typep obj 'user) obj))) (defun login-user (username password) (when-let (user (user-with-name username)) (with-slots (pwhash pwsalt) user (when (equalp pwhash (hash-string password pwsalt)) user)))) (defun last-change (obj) (slot-value obj 'bknr.datastore::last-change)) (defun recent-playlists (&optional (count 10)) (take count (sort (copy-seq (store-objects-with-class 'playlist)) #'> :key #'last-change))) (defun playlist-duration (pl) (reduce #'+ (playlist-tracks pl) :key (lambda (tr) (or (track-duration tr) 0)) :initial-value 0)) (defun add-track (tr pl &optional (n -1)) (setf (playlist-tracks pl) (insert-nth tr n (playlist-tracks pl)))) (defun remove-nth-from-playlist (pl n) (multiple-value-bind (newlist track) (remove-nth n (playlist-tracks pl) t) (setf (playlist-tracks pl) newlist (track-playlists track) (delete pl (track-playlists track) :test #'eq :count 1)))) (defun track-with-source (source) (find source (store-objects-with-class 'track) :test #'string-equal :key 'track-source)) ;;; TRANSACTIONS (defun new-user (&key name) (with-transaction () (make-instance 'user :name name))) (defun set-new-password (user pw) (with-transaction () (setf (user-pwhash user) (hash-string pw (user-pwsalt user))) t)) (defun use-invite-with-code (code username pw) (with-transaction () (when-let (invite (invite-by-code code)) (when (uses-remaining invite) (decf (uses-remaining invite)) (unless (plusp (uses-remaining invite)) (bknr.datastore:delete-object invite))) (let ((user (make-instance 'user :name username))) (setf (user-pwhash user) (hash-string pw (user-pwsalt user))) user)))) (defun remove-editor (playlist editor) (with-transaction () (setf (playlist-editors playlist) (delete editor (playlist-editors playlist))))) (defun add-editor (playlist editor) (with-transaction () (pushnew editor (playlist-editors playlist) :test #'eq))) (defun destroy-invite (invite) (with-transaction () (bknr.datastore:delete-object invite))) (defun make-invite (user &optional uses) (with-transaction () (make-instance 'invite :maker user :uses-remaining uses))) (defun append-track (pl tr) (with-transaction () (add-track tr pl))) (defun delete-track-at (pl pos) (when-let (tr (nth pos (playlist-tracks pl))) (with-transaction () (setf (playlist-tracks pl) (delete tr (playlist-tracks pl))) t))) (defun swap-tracks (pl n m) (unless (or (minusp (min m n)) (>= (max m n) (length (playlist-tracks pl)) )) (with-transaction () (setf (playlist-tracks pl) (nswap (playlist-tracks pl) n m))))) (defun new-track (file trackinfo) "Trackinfo is a plist containing information about the track to create." (with-transaction () (let ((track (apply #'make-instance 'track trackinfo))) (setf (track-file track) (namestring file)) track))) (defun new-playlist (user &key title) (with-transaction () (make-instance 'playlist :title title :user user))) (defun destroy-playlist (pl) (with-transaction () (setf (user-playlists (playlist-user pl)) (delete pl (user-playlists (playlist-user pl)))) (bknr.datastore:delete-object pl))) (defun update-playlist-title (playlist title) (with-transaction () (setf (playlist-title playlist) title)))