;; Author: Leonardo Tamiano
;; MIT License
;; Copyright (c) 2024 Leonardo Tamiano
;; Permission is hereby granted, free of charge, to any person obtaining a copy
;; of this software and associated documentation files (the "Software"), to deal
;; in the Software without restriction, including without limitation the rights
;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
;; copies of the Software, and to permit persons to whom the Software is
;; furnished to do so, subject to the following conditions:
;; The above copyright notice and this permission notice shall be included in all
;; copies or substantial portions of the Software.
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
;; SOFTWARE.
;; -------------------------------------------------------------------------------------
(setq yt2git/metadata-file "youtube.org")
(setq yt2git/repos
'(
((:name . "yt-it")
(:label . "IT")
(:private . "/home/leo/projects/FOUNDATIONS/yt-it")
(:public . "/home/leo/projects/PUBLIC/yt-it"))
((:name . "yt-en")
(:label . "EN")
(:private . "/home/leo/projects/FOUNDATIONS/yt-en")
(:public . "/home/leo/projects/PUBLIC/yt-en"))
))
(defun yt2git/label2txt (label)
(assoc-default
label
'(
(:title . "TITLE")
(:playlist . "PLAYLIST")
(:pubblication . "PUBBLICATION")
(:youtube . "YOUTUBE")
(:public . "PUBLIC")
(:private . "PRIVATE")
(:tbd . "TBD")
))
)
(defun yt2git/get-prop (field repo)
(assoc-default field repo))
;; -------------------------------------------------------------------------------------
(defun yt2git/gen-rss ()
(interactive)
(yt2git/gen-rss-by-repo (yt2git/select-repository))
)
(defun yt2git/update-rss ()
(interactive)
(yt2git/update-rss-by-repo (yt2git/select-repository))
)
(defun yt2git/select-repository ()
(let* ((repo-name (ivy-read "Project: "
(mapcar
(lambda (repo) (assoc-default :name repo))
yt2git/repos)))
(repository (car (seq-filter
(lambda (e) (string= (assoc-default :name e) repo-name))
yt2git/repos))))
repository))
;; -------------------------------------------------------------------------------------
(defun yt2git/gen-rss-by-repo (repo)
(let* ((public (yt2git/get-prop :public repo))
(rss-feed (concat public "/feed/rss.xml")))
(with-temp-buffer
;; initial tags
(insert
(concat "\n\n"
"\n"
(format "Leonardo Tamiano - Youtube %s \n" (yt2git/get-prop :label repo))
" https://www.youtube.com/@LT123/videos \n"
" Aggiornamenti e metadati sui video caricati \n"
(format "\n" (assoc-default :name repo))
"\n"))
;; one for each video
(mapcar
(lambda (video) (yt2git/gen-rss-entry repo video))
(yt2git/videos repo))
;; closing tags
(insert "\n")
(insert "")
(write-file rss-feed)
)
))
(defun yt2git/gen-rss-entry (repo video)
(let* ((delim "\n\n-------------------------\n\n")
(description
(with-temp-buffer
(insert (yt2git/gen-description-by-video repo video))
(beginning-of-buffer)
(replace-string "\n" " \n")
(beginning-of-buffer)
(replace-regexp "\\(https://.*\\) " " \\1 ")
(beginning-of-buffer)
(replace-regexp "\\(http://.*\\) " " \\1 ")
(buffer-string))))
(insert
(concat
"\n"
"" (assoc-default :title video) "\n"
"" (format "https://leonardotamiano.xyz/rss/%s.xml#" (assoc-default :name repo)) (assoc-default :youtube video) "\n"
"" (format "https://youtu.be/%s" (assoc-default :youtube video)) "\n"
"" (yt2git/format-rss-date (assoc-default :pubblication video)) "\n"
""
(format "" description)
"\n"
"\n\n"
))
)
)
(defun yt2git/format-rss-date (custom-date)
(format-time-string "%a, %d %b %Y 00:00:00 GMT" (date-to-time custom-date))
)
;;
;; With this function we can generate a custom description for a
;; specific video.
;;
(defun yt2git/gen-description-by-video (repo video)
(let* ((org-file (concat (assoc-default :private repo) "/" yt2git/metadata-file))
(query (concat (yt2git/label2txt :youtube) "=\"" (assoc-default :youtube video) "\""))
(labels (if (string= (assoc-default :label repo) "IT")
(list '(:ref . "RIFERIMENTI") '(:contacts . "CONTATTI"))
(list '(:ref . "REFERENCES") '(:contacts . "CONTACTS"))))
;; First we extract the raw data from the file. To do this we
;; first select the outline based on the YOUTUBE-ID, and then
;; we iterate over all entries of that outline.
;;
(raw-data (car (org-map-entries
(lambda ()
(org-map-entries
(lambda () `(,(org-get-heading) . ,(org-get-entry)))
t 'tree))
query
(list org-file))))
;;
;; Then we extract description. timestamp and references from
;; the various outlines.
;;
;; NOTE: for this to work properly we need to have this exact
;; order when writing the org file. That is, first we write
;; description, then timestamp and finally the references.
;;
(raw-description (cdadr raw-data))
(raw-timestamp (cdaddr raw-data))
(raw-references (cdr (cadddr raw-data)))
(template (format (concat
"%s"
"\n\n-------------------------\n\n"
"TIMESTAMP"
"\n\n"
"%s"
"\n\n-------------------------\n\n"
;; "REFERENCES"
(assoc-default :ref labels)
"\n\n"
"- Material: https://github.com/LeonardoE95/%s/tree/main/src/%s-%s\n"
"%s"
"\n\n-------------------------\n\n"
;; "CONTACTS"
(assoc-default :contacts labels)
"\n\n"
"- Blog: https://blog.leonardotamiano.xyz/\n"
"- Github: https://github.com/LeonardoE95?tab=repositories\n"
"- Support: https://www.paypal.com/donate/?hosted_button_id=T49GUPRXALYTQ\n"
)
raw-description
raw-timestamp
(assoc-default :name repo)
(assoc-default :pubblication video)
(assoc-default :public video)
raw-references))
(full-description (with-temp-buffer
(insert template)
(beginning-of-buffer)
(delete-matching-lines "begin_example")
(delete-matching-lines "end_example")
(buffer-string)
)))
(kill-new full-description)
full-description
))
(defun yt2git/videos (repo)
(let ((org-file (concat (assoc-default :private repo) "/" yt2git/metadata-file))
(query (concat (yt2git/label2txt :youtube) "={.+}")))
(with-temp-buffer
;;
;; read youtube.org and extract over all org-mode outlines and select
;; only those that have a YOUTUBE property assigned. From the
;; org-entries only extract the things we care about to construct our
;; ToC, which are the following values
;;
;; Title, Playlist Pubblication Date, Video
;;
(setq-local entries (org-map-entries
(lambda ()
(let ((props (org-entry-properties)))
(list `(:title . ,(assoc-default (yt2git/label2txt :title) props))
`(:playlist . ,(assoc-default (yt2git/label2txt :playlist) props))
`(:pubblication . ,(assoc-default (yt2git/label2txt :pubblication) props))
`(:youtube . ,(assoc-default (yt2git/label2txt :youtube) props))
`(:private . ,(assoc-default (yt2git/label2txt :private) props))
`(:public . ,(assoc-default (yt2git/label2txt :public) props)))
))
query
(list org-file)
))
;; do not show all entries that have not yet received a
;; pubblication date or that have not been published yet.
;;
(setq-local entries (seq-filter
(lambda (e)
(and
(not (string= (assoc-default :tbd e)
(assoc-default :pubblication e)))
(org-string<= (assoc-default :pubblication e)
(format-time-string "%Y-%m-%d"))))
entries))
;; sorts using the pubblication date, ordering them from most
;; recent video to the oldest one. Notice that while sort performs
;; an in-place sort, the reverse function instead creates a new
;; list and returns the reversed list.
(setq-local entries (reverse
(sort entries (lambda (a b)
(string< (assoc-default :pubblication a)
(assoc-default :pubblication b)
)))))
entries
)
)
)
;; -------------------------------------------------------------------------------------
(defun yt2git/update-rss-by-repo (repo)
(let* ((public (yt2git/get-prop :public repo))
(rss-feed-filename (concat public "/feed/rss.xml"))
(rss-feed-content (with-temp-buffer
(insert-file-contents rss-feed-filename)
(buffer-string)
))
;; get to the insertion point for new videos (after \n\n)
(insertion-point (with-temp-buffer
(insert rss-feed-content)
(beginning-of-buffer)
(search-forward "/>\n\n")
(point)
))
;; extract latest pubDate as a reference point
(latest-video-date
(yt2git/format-custom-date
(with-temp-buffer
(insert rss-feed-content)
(beginning-of-buffer)
(search-forward "")
(buffer-substring (point) (+ (point) 29)))))
;; get all videos published after that pubDate
(new-videos (yt2git/videos-by-date repo latest-video-date))
)
;; for each video add a new item
(with-temp-buffer
(insert-file-contents rss-feed-filename)
(goto-char insertion-point)
(mapcar
(lambda (video) (yt2git/gen-rss-entry repo video))
new-videos)
(write-file rss-feed-filename)
)
)
)
;; only returns videos that are returned AFTER the given date.
(defun yt2git/videos-by-date (repo date)
(let ((org-file (concat (yt2git/get-prop :private repo) "/" yt2git/metadata-file))
(query (concat (yt2git/label2txt :youtube) "={.+}")))
(with-temp-buffer
;;
;; read youtube.org and extract over all org-mode outlines and select
;; only those that have a YOUTUBE property assigned. From the
;; org-entries only extract the things we care about to construct our
;; ToC, which are the following values
;;
;; Title, Playlist Pubblication Date, Video
;;
(setq-local entries (org-map-entries
(lambda ()
(let ((props (org-entry-properties)))
(list `(:title . ,(yt2git/get-prop (yt2git/label2txt :title) props))
`(:playlist . ,(yt2git/get-prop (yt2git/label2txt :playlist) props))
`(:pubblication . ,(yt2git/get-prop (yt2git/label2txt :pubblication) props))
`(:youtube . ,(yt2git/get-prop (yt2git/label2txt :youtube) props))
`(:private . ,(yt2git/get-prop (yt2git/label2txt :private) props))
`(:public . ,(yt2git/get-prop (yt2git/label2txt :public) props)))
))
query
(list org-file)
))
;; do not show all entries that have not yet received a
;; pubblication date or that have not been published yet.
;;
(setq-local entries (seq-filter
(lambda (e)
(and
(not (string= (yt2git/get-prop :tbd e)
(yt2git/get-prop :pubblication e)))
(org-string<= (yt2git/get-prop :pubblication e)
(format-time-string "%Y-%m-%d"))
;; THIS IS THE ONLY THING WE'VE ADDED IN THIS SPECIAL VERSION.
(org-string> (yt2git/get-prop :pubblication e)
date)
)
)
entries))
;; sorts using the pubblication date, ordering them from most
;; recent video to the oldest one. Notice that while sort performs
;; an in-place sort, the reverse function instead creates a new
;; list and returns the reversed list.
(setq-local entries (reverse
(sort entries (lambda (a b)
(string< (yt2git/get-prop :pubblication a)
(yt2git/get-prop :pubblication b)
)))))
entries
)
)
)