#.(progn (asdf:defsystem :fwoar.gallery :depends-on (:lquery :araneus :drakma :clack :yason :ningle :lass)) (ql:quickload :fwoar.gallery) nil) (defpackage :fwoar.gallery (:use :cl :araneus :alexandria :serapeum) (:export )) (in-package :fwoar.gallery) (defvar *url*) (defvar *gallery*) (defmacro new (class &rest initializer-syms) `(make-instance ,class ,@(mapcan (serapeum:op (list (alexandria:make-keyword _1) _1)) initializer-syms))) (defclass gallery () ((%images :initarg :images :initform () :reader images))) (defun make-gallery (images) (new 'gallery images)) (defclass page () ((%gallery :initarg :gallery :reader gallery) (%page :initarg :page :initform 1 :reader page))) (defun make-page (gallery page) (new 'page gallery page)) (defclass image () ((%url :initarg :url :initform (error "need url for image") :reader url))) (defclass video () ((%url :initarg :url :initform (error "need url for video") :reader url))) (defmethod print-object ((o image) s) (format s "#.(make-image \"~a\")" (url o))) (defmethod print-object ((o video) s) (format s "#.(make-video \"~a\")" (url o))) (defmethod print-object ((o gallery) s) (if *print-readably* (format s "#.(make-gallery (list ~{~a~^~%~22t~}))" (images o)) (format s "#.(make-gallery (~a images))" (length (images o))))) (defun make-image (url) (new 'image url)) (defun make-video (url) (new 'video url)) (defun page->gallery (dom base-url) (check-type dom plump:node) (make-gallery (coerce (lquery:$ (inline dom) "a" (attr "href") (filter (lambda (u) (search "jpg" u))) (filter (complement (lambda (u) (search "thumb" u)))) (map (lambda (u) (with-output-to-string (s) (puri:render-uri (puri:merge-uris u base-url) s)))) (map 'make-image)) 'list))) (define-controller root (params gallery) (let ((page (make-page gallery (parse-integer (or (cdr (assoc "page" params :test #'equalp)) "1"))))) page)) (defmacro hostname-case (uri &body cases) `(serapeum:string-case (puri:uri-host ,uri) ,@cases)) (defun get-site (uri) (hostname-case uri ("gfycat.com" :gfycat) ("imgur.com" :imgur) ("i.redd.it" :ireddit) ("v.redd.it" :vreddit))) (defgeneric transform-url (site uri) (:method (_ uri) (declare (ignore _)) (make-image (puri:render-uri uri nil))) (:method ((site (eql :gfycat)) uri) (make-video (format nil "~a" (puri:render-uri (fw.lu:prog1-bind (uri (puri:copy-uri uri)) (setf (puri:uri-host uri) "gfycat.com" (puri:uri-path uri) (format nil "/ifr~a" (puri:uri-path uri)))) nil)))) (:method ((site (eql :imgur)) uri) (make-image (format nil "~a.jpg" (puri:render-uri (fw.lu:prog1-bind (uri (puri:copy-uri uri)) (setf (puri:uri-host uri) "i.imgur.com")) nil)))) (:method ((site (eql :vreddit)) uri) (make-video (puri:render-uri (fw.lu:prog1-bind (uri (puri:copy-uri uri)) (setf (puri:uri-path uri) (format nil "~a/HLSPlaylist.m3u8" (puri:uri-path uri)))) nil)))) (defun ensure-uri (uri) (etypecase uri (string (puri:parse-uri uri)) (puri:uri uri))) (defun process-uri-list (uris) (loop for raw-uri in uris for uri = (ensure-uri raw-uri) for transformed = (transform-url (get-site uri) uri) when transformed collect transformed)) (spinneret:deftag css (body attrs) `(:style :type "text/css" ,@attrs (:raw ,(format nil "~%") ,(apply 'lass:compile-and-write body) ))) (spinneret:deftag js (body attrs) `(:script :type "text/javascript" ,@attrs (:raw ,(format nil "~%") (ps:ps ,@body)))) (defun gallery-css () (spinneret:with-html (css (:let ((bottom-gap 2em) (gallery-height (calc (- 100vh 2em)))) (* :box-sizing border-box) (html :outline "thin solid black") ((:or body html div section) :margin 0 :padding 0) (.gallery :display block :background "#888" :height #(gallery-height) :overflow-y scroll :scroll-snap-type "y mandatory" :flex-wrap wrap) ((.gallery img) :width 100% :height 100% :scroll-snap-align start :object-fit contain) ((.gallery video) :width 100% :height 100% :scroll-snap-align start :object-fit contain) ((.gallery iframe) :width 100% :height 100% :scroll-snap-align start :object-fit contain) )))) (defun gallery-js () (js (ps:chain #() for-each (call (ps:chain document (query-selector-all ".gallery > div")) (lambda (it) (ps:chain it (add-event-listener "click" (lambda () (ps:chain #() for-each (call (ps:chain document (query-selector-all ".expanded")) (lambda (other) (unless (eql other it) (ps:chain other class-list (remove "expanded")))))) (ps:chain it class-list (toggle "expanded"))))))))) ) (defmethod view :around ((name (eql 'root)) (model page)) (spinneret:with-html-string (:html (:head (gallery-css)) (:body (let ((*gallery* (make-gallery (subseq (images (gallery model)) (* 52 (1- (page model))) (min (* 52 (page model)) (length (images (gallery model)))))))) (call-next-method)) (when (<= (* 52 (page model)) (length (images (gallery model)))) (:a :href (format nil "/?page=~d" (1+ (page model))) :style "width: 100%; text-align: center; display: inline-block;" "next")) (gallery-js))))) (define-view root ((model page)) (spinneret:with-html (:section.gallery (loop for img in (images *gallery*) do (call-current-view img))))) (define-view root ((model image)) (spinneret:with-html (:img :src (url model)))) (define-view root ((model video)) (spinneret:with-html (if (string-contains-p "/ifr/" (url model)) (:iframe :src (url model)) (:video :autoplay "autoplay" :loop "loop" (:source :src (url model) :type "video/mp4"))))) (defun initialize-app (app gallery) (defroutes app (("/") (as-route 'root :gallery gallery)))) (defun get-reddit-items (r) (process-uri-list (mapcar (lambda (i) (fw.lu:dive '("data" "url") i)) (fw.lu:dive '("data" "children") r)))) (defun main (url) (let* ((app (make-instance 'ningle:)) (gallery (page->gallery (plump:parse (babel:octets-to-string (drakma:http-request url :force-binary t) :encoding :latin-1)) url))) (initialize-app app gallery) (clack:clackup app))) (defun reddit-main (subreddits) (let* ((app (make-instance 'ningle:)) (gallery (make-gallery (mapcan (lambda (subreddit) (get-reddit-items (yason:parse (babel:octets-to-string (drakma:http-request (format nil "https://reddit.com/r/~a.json" subreddit) :force-binary t) :encoding :latin-1)))) subreddits)))) (setf *gallery* gallery) (initialize-app app gallery) (clack:clackup app))) (defun cl-user::fwoar.gallery.main (version init) (ecase version (:reddit (reddit-main init)) (:dir (main init))))