From 731529ddb5dc437e4d3f360bc64465531f6460ec Mon Sep 17 00:00:00 2001 From: Daniel Nitsikopoulos Date: Sat, 2 Dec 2023 13:55:22 +1100 Subject: [PATCH] Podcast scrobbling --- Rakefile | 7 + app/assets/builds/tailwind.css | 206 ++++++++---------- app/relations/podcast_scrobbles.rb | 11 + app/repos/podcast_scrobble_repo.rb | 20 ++ app/templates/podcasts/index.html.slim | 8 + app/views/podcasts/index.rb | 6 +- ...20231130095816_create_podcast_scrobbles.rb | 17 ++ lib/adamantium/overcast_scrobbler.rb | 46 ++++ 8 files changed, 203 insertions(+), 118 deletions(-) create mode 100644 app/relations/podcast_scrobbles.rb create mode 100644 app/repos/podcast_scrobble_repo.rb create mode 100644 db/migrate/20231130095816_create_podcast_scrobbles.rb create mode 100644 lib/adamantium/overcast_scrobbler.rb diff --git a/Rakefile b/Rakefile index 06234b5..2a0bcb0 100644 --- a/Rakefile +++ b/Rakefile @@ -30,6 +30,13 @@ namespace :blog do end end + task scrobble_podcasts: ["blog:load_environment"] do + require "hanami/prepare" + + command = Adamantium::OvercastScrobbler.new(username: "daniel@dnitza.com", password: "impacted-mingle.buckeye4incise") + command.() + end + task load_from_bookshelf: ["blog:load_environment"] do require "hanami/prepare" require "csv" diff --git a/app/assets/builds/tailwind.css b/app/assets/builds/tailwind.css index ee24982..0590c47 100644 --- a/app/assets/builds/tailwind.css +++ b/app/assets/builds/tailwind.css @@ -1082,6 +1082,10 @@ video { margin-bottom: 0px; } +.mb-1 { + margin-bottom: 0.25rem; +} + .mb-12 { margin-bottom: 3rem; } @@ -1114,10 +1118,6 @@ video { margin-left: 0.375rem; } -.ml-2 { - margin-left: 0.5rem; -} - .ml-\[5\] { margin-left: 5; } @@ -1278,10 +1278,6 @@ video { width: 100vw; } -.w-64 { - width: 16rem; -} - .max-w-3xl { max-width: 48rem; } @@ -1302,6 +1298,10 @@ video { flex: 1 1 0%; } +.flex-auto { + flex: 1 1 auto; +} + .flex-initial { flex: 0 1 auto; } @@ -1310,22 +1310,10 @@ video { flex: none; } -.flex-auto { - flex: 1 1 auto; -} - .grow { flex-grow: 1; } -.basis-1\/4 { - flex-basis: 25%; -} - -.basis-full { - flex-basis: 100%; -} - .basis-auto { flex-basis: auto; } @@ -1365,6 +1353,10 @@ video { grid-auto-flow: row; } +.grid-flow-col { + grid-auto-flow: column; +} + .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -1389,6 +1381,10 @@ video { grid-template-rows: repeat(1, minmax(0, 1fr)); } +.grid-rows-2 { + grid-template-rows: repeat(2, minmax(0, 1fr)); +} + .flex-row { flex-direction: row; } @@ -1405,6 +1401,10 @@ video { align-items: center; } +.justify-end { + justify-content: flex-end; +} + .justify-center { justify-content: center; } @@ -1421,6 +1421,16 @@ video { gap: 1rem; } +.gap-x-1 { + -moz-column-gap: 0.25rem; + column-gap: 0.25rem; +} + +.gap-x-1\.5 { + -moz-column-gap: 0.375rem; + column-gap: 0.375rem; +} + .space-x-1 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.25rem * var(--tw-space-x-reverse)); @@ -1587,6 +1597,16 @@ video { background-color: rgb(224 231 255 / var(--tw-bg-opacity)); } +.bg-indigo-300 { + --tw-bg-opacity: 1; + background-color: rgb(165 180 252 / var(--tw-bg-opacity)); +} + +.bg-indigo-50 { + --tw-bg-opacity: 1; + background-color: rgb(238 242 255 / var(--tw-bg-opacity)); +} + .bg-orange-100 { --tw-bg-opacity: 1; background-color: rgb(255 237 213 / var(--tw-bg-opacity)); @@ -1617,28 +1637,14 @@ video { background-color: rgb(255 255 255 / var(--tw-bg-opacity)); } -.bg-yellow-100\/60 { - background-color: rgb(254 249 195 / 0.6); +.bg-pink-400 { + --tw-bg-opacity: 1; + background-color: rgb(244 114 182 / var(--tw-bg-opacity)); } -.bg-indigo-50 { +.bg-pink-200 { --tw-bg-opacity: 1; - background-color: rgb(238 242 255 / var(--tw-bg-opacity)); -} - -.bg-indigo-400 { - --tw-bg-opacity: 1; - background-color: rgb(129 140 248 / var(--tw-bg-opacity)); -} - -.bg-indigo-600 { - --tw-bg-opacity: 1; - background-color: rgb(79 70 229 / var(--tw-bg-opacity)); -} - -.bg-indigo-300 { - --tw-bg-opacity: 1; - background-color: rgb(165 180 252 / var(--tw-bg-opacity)); + background-color: rgb(251 207 232 / var(--tw-bg-opacity)); } .bg-opacity-75 { @@ -1736,6 +1742,10 @@ video { padding-right: 2rem; } +.pt-1 { + padding-top: 0.25rem; +} + .pt-2 { padding-top: 0.5rem; } @@ -1744,22 +1754,6 @@ video { padding-top: 1rem; } -.pr-1 { - padding-right: 0.25rem; -} - -.pr-4 { - padding-right: 1rem; -} - -.pt-1 { - padding-top: 0.25rem; -} - -.pt-1\.5 { - padding-top: 0.375rem; -} - .text-left { text-align: left; } @@ -1796,10 +1790,6 @@ video { font-size: 1.25rem; } -.text-xsm { - font-size: 0.75rem; -} - .font-bold { font-weight: 700; } @@ -1816,10 +1806,6 @@ video { text-transform: uppercase; } -.leading-10 { - line-height: 2.5rem; -} - .leading-6 { line-height: 1.5rem; } @@ -1903,11 +1889,21 @@ video { color: rgb(129 140 248 / var(--tw-text-opacity)); } +.text-indigo-900 { + --tw-text-opacity: 1; + color: rgb(49 46 129 / var(--tw-text-opacity)); +} + .text-orange-100 { --tw-text-opacity: 1; color: rgb(255 237 213 / var(--tw-text-opacity)); } +.text-pink-400 { + --tw-text-opacity: 1; + color: rgb(244 114 182 / var(--tw-text-opacity)); +} + .text-pink-600 { --tw-text-opacity: 1; color: rgb(219 39 119 / var(--tw-text-opacity)); @@ -1928,14 +1924,14 @@ video { color: rgb(255 255 255 / var(--tw-text-opacity)); } -.text-indigo-50 { +.text-pink-900 { --tw-text-opacity: 1; - color: rgb(238 242 255 / var(--tw-text-opacity)); + color: rgb(131 24 67 / var(--tw-text-opacity)); } -.text-indigo-900 { +.text-pink-800 { --tw-text-opacity: 1; - color: rgb(49 46 129 / var(--tw-text-opacity)); + color: rgb(157 23 77 / var(--tw-text-opacity)); } .underline { @@ -2172,11 +2168,6 @@ h1, h2, h3, h4, h5, h6, h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { border-color: rgb(37 99 235 / var(--tw-border-opacity)); } -.hover\:border-blue-800:hover { - --tw-border-opacity: 1; - border-color: rgb(30 64 175 / var(--tw-border-opacity)); -} - .hover\:bg-blue-100:hover { --tw-bg-opacity: 1; background-color: rgb(219 234 254 / var(--tw-bg-opacity)); @@ -2192,11 +2183,6 @@ h1, h2, h3, h4, h5, h6, h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { background-color: rgb(147 197 253 / var(--tw-bg-opacity)); } -.hover\:bg-blue-800:hover { - --tw-bg-opacity: 1; - background-color: rgb(30 64 175 / var(--tw-bg-opacity)); -} - .hover\:bg-emerald-100:hover { --tw-bg-opacity: 1; background-color: rgb(209 250 229 / var(--tw-bg-opacity)); @@ -2237,11 +2223,6 @@ h1, h2, h3, h4, h5, h6, h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { background-color: rgb(254 226 226 / var(--tw-bg-opacity)); } -.hover\:bg-yellow-200:hover { - --tw-bg-opacity: 1; - background-color: rgb(254 240 138 / var(--tw-bg-opacity)); -} - .hover\:fill-blue-400:hover { fill: #60a5fa; } @@ -2254,11 +2235,6 @@ h1, h2, h3, h4, h5, h6, h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { fill: #c084fc; } -.hover\:text-blue-100:hover { - --tw-text-opacity: 1; - color: rgb(219 234 254 / var(--tw-text-opacity)); -} - .hover\:text-blue-400:hover { --tw-text-opacity: 1; color: rgb(96 165 250 / var(--tw-text-opacity)); @@ -2304,6 +2280,11 @@ h1, h2, h3, h4, h5, h6, h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: rgb(244 114 182 / var(--tw-text-opacity)); } +.hover\:text-pink-600:hover { + --tw-text-opacity: 1; + color: rgb(219 39 119 / var(--tw-text-opacity)); +} + .hover\:text-red-400:hover { --tw-text-opacity: 1; color: rgb(248 113 113 / var(--tw-text-opacity)); @@ -2313,6 +2294,10 @@ h1, h2, h3, h4, h5, h6, h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { text-decoration-line: underline; } +.hover\:decoration-wavy:hover { + text-decoration-style: wavy; +} + .hover\:opacity-80:hover { opacity: 0.8; } @@ -2463,10 +2448,6 @@ h1, h2, h3, h4, h5, h6, h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { background-color: rgb(251 191 36 / var(--tw-bg-opacity)); } - .dark\:bg-amber-400\/60 { - background-color: rgb(251 191 36 / 0.6); - } - .dark\:bg-black { --tw-bg-opacity: 1; background-color: rgb(0 0 0 / var(--tw-bg-opacity)); @@ -2506,6 +2487,11 @@ h1, h2, h3, h4, h5, h6, h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { background-color: rgb(17 24 39 / var(--tw-bg-opacity)); } + .dark\:bg-indigo-400 { + --tw-bg-opacity: 1; + background-color: rgb(129 140 248 / var(--tw-bg-opacity)); + } + .dark\:bg-indigo-950 { --tw-bg-opacity: 1; background-color: rgb(30 27 75 / var(--tw-bg-opacity)); @@ -2531,21 +2517,6 @@ h1, h2, h3, h4, h5, h6, h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { background-color: rgb(15 23 42 / var(--tw-bg-opacity)); } - .dark\:bg-indigo-300 { - --tw-bg-opacity: 1; - background-color: rgb(165 180 252 / var(--tw-bg-opacity)); - } - - .dark\:bg-indigo-400 { - --tw-bg-opacity: 1; - background-color: rgb(129 140 248 / var(--tw-bg-opacity)); - } - - .dark\:text-amber-100 { - --tw-text-opacity: 1; - color: rgb(254 243 199 / var(--tw-text-opacity)); - } - .dark\:text-amber-500 { --tw-text-opacity: 1; color: rgb(245 158 11 / var(--tw-text-opacity)); @@ -2596,11 +2567,6 @@ h1, h2, h3, h4, h5, h6, h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: rgb(75 85 99 / var(--tw-text-opacity)); } - .dark\:text-gray-800 { - --tw-text-opacity: 1; - color: rgb(31 41 55 / var(--tw-text-opacity)); - } - .dark\:text-indigo-300 { --tw-text-opacity: 1; color: rgb(165 180 252 / var(--tw-text-opacity)); @@ -2651,6 +2617,16 @@ h1, h2, h3, h4, h5, h6, h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: rgb(255 255 255 / var(--tw-text-opacity)); } + .dark\:text-pink-200 { + --tw-text-opacity: 1; + color: rgb(251 207 232 / var(--tw-text-opacity)); + } + + .dark\:text-pink-100 { + --tw-text-opacity: 1; + color: rgb(252 231 243 / var(--tw-text-opacity)); + } + .dark\:shadow-pink-200 { --tw-shadow-color: #fbcfe8; --tw-shadow: var(--tw-shadow-colored); @@ -2741,10 +2717,6 @@ h1, h2, h3, h4, h5, h6, h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { border-color: rgb(15 23 42 / var(--tw-border-opacity)); } - .dark\:hover\:bg-amber-400\/80:hover { - background-color: rgb(251 191 36 / 0.8); - } - .dark\:hover\:bg-emerald-800:hover { --tw-bg-opacity: 1; background-color: rgb(6 95 70 / var(--tw-bg-opacity)); @@ -2790,9 +2762,9 @@ h1, h2, h3, h4, h5, h6, h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { background-color: rgb(131 24 67 / var(--tw-bg-opacity)); } - .dark\:hover\:text-yellow-100:hover { + .dark\:hover\:text-pink-100:hover { --tw-text-opacity: 1; - color: rgb(254 249 195 / var(--tw-text-opacity)); + color: rgb(252 231 243 / var(--tw-text-opacity)); } .hover\:dark\:text-blue-100:hover { diff --git a/app/relations/podcast_scrobbles.rb b/app/relations/podcast_scrobbles.rb new file mode 100644 index 0000000..0404a4b --- /dev/null +++ b/app/relations/podcast_scrobbles.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Adamantium + module Relations + class PodcastScrobbles < ROM::Relation[:sql] + schema :podcast_scrobbles, infer: true + + auto_struct(true) + end + end +end diff --git a/app/repos/podcast_scrobble_repo.rb b/app/repos/podcast_scrobble_repo.rb new file mode 100644 index 0000000..bfcbf65 --- /dev/null +++ b/app/repos/podcast_scrobble_repo.rb @@ -0,0 +1,20 @@ +module Adamantium + module Repos + class PodcastScrobbleRepo < Adamantium::Repo[:podcast_scrobbles] + commands :create + + def exists?(id:) + !!podcast_scrobbles + .where(overcast_id: id) + .one + end + + def listing + podcast_scrobbles + .order(Sequel.desc(:listened_at)) + .limit(5) + .to_a + end + end + end +end diff --git a/app/templates/podcasts/index.html.slim b/app/templates/podcasts/index.html.slim index 26c0ba4..407c929 100644 --- a/app/templates/podcasts/index.html.slim +++ b/app/templates/podcasts/index.html.slim @@ -4,6 +4,14 @@ div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark: h1 🎙️ Podcasts div class="mb-12 max-w-prose mx-auto" + - if listens && listens.count > 0 + div class="p-4 bg-pink-200 dark:bg-pink-900 rounded" + h4 class="p-0 m-0 text-pink-900 dark:text-pink-200" Recent listens + - listens.each do |listen| + div + a class="text-pink-800 dark:text-pink-100 no-underline hover:decoration-wavy hover:underline" href=listen.url + = "#{listen.podcast_name}: #{listen.title}" + table class="prose dark:prose-invert table-auto" thead tr diff --git a/app/views/podcasts/index.rb b/app/views/podcasts/index.rb index fc21702..e04a868 100644 --- a/app/views/podcasts/index.rb +++ b/app/views/podcasts/index.rb @@ -2,11 +2,15 @@ module Adamantium module Views module Podcasts class Index < View - include Deps["repos.podcast_repo"] + include Deps["repos.podcast_repo", "repos.podcast_scrobble_repo"] expose :podcasts do podcast_repo.listing end + + expose :listens do + podcast_scrobble_repo.listing + end end end end diff --git a/db/migrate/20231130095816_create_podcast_scrobbles.rb b/db/migrate/20231130095816_create_podcast_scrobbles.rb new file mode 100644 index 0000000..df471a2 --- /dev/null +++ b/db/migrate/20231130095816_create_podcast_scrobbles.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + create_table :podcast_scrobbles do + primary_key :id + column :overcast_id, :text + column :podcast_name, :text + column :title, :text + column :url, :text + column :enclosure_url, :text + column :listened_at, :date + + index :overcast_id + end + end +end diff --git a/lib/adamantium/overcast_scrobbler.rb b/lib/adamantium/overcast_scrobbler.rb new file mode 100644 index 0000000..fb4a446 --- /dev/null +++ b/lib/adamantium/overcast_scrobbler.rb @@ -0,0 +1,46 @@ +require "httparty" +require "nokogiri" +require "down" + +module Adamantium + class OvercastScrobbler + def initialize(username:, password:) + @username = username + @password = password + end + + def call + repo = Adamantium::Container["repos.podcast_scrobble_repo"] + response = HTTParty.post("https://overcast.fm/login", body: {email: @username, password: @password}) + cookie = response.request.options[:headers]["Cookie"] + + opml_file = Down::NetHttp.download("https://overcast.fm/account/export_opml/extended", headers: {"Cookie" => cookie}) + doc = File.open(opml_file) { |f| Nokogiri::XML(f) } + + podcast_list = [] + + doc.xpath("//outline[@type='rss']").each_with_object(podcast_list) do |rss, memo| + podcasts = rss.xpath("outline[@type='podcast-episode']").select{|ep| ep.get_attribute("played") == "1" } + + name = rss.get_attribute("title") + + podcasts.each do |episode| + attrs = {} + attrs[:overcast_id] = episode.get_attribute("overcastId") + attrs[:podcast_name] = name + attrs[:title] = episode.get_attribute("title") + attrs[:url] = episode.get_attribute("url") + attrs[:enclosure_url] = episode.get_attribute("enclosureUrl") + attrs[:listened_at] = episode.get_attribute("userUpdatedDate") + + memo << attrs + end + + podcast_list.each do |podcast| + next if repo.exists?(id: podcast[:overcast_id]) + repo.create(podcast) + end + end + end + end +end