Podcast scrobbling

This commit is contained in:
2023-12-02 13:55:22 +11:00
parent c0c50bc107
commit 731529ddb5
8 changed files with 203 additions and 118 deletions

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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