mirror of
https://github.com/mistergibson/radio.git
synced 2026-09-09 06:19:52 -07:00
a bit more self-contained
This commit is contained in:
parent
3feb6fe5b1
commit
6250104ff5
2 changed files with 105 additions and 95 deletions
|
|
@ -11,6 +11,18 @@
|
||||||
# Politeness: audio/video verdict cached in shows.media_class; gpodder OPML
|
# Politeness: audio/video verdict cached in shows.media_class; gpodder OPML
|
||||||
# pull uses bounded retry with exponential backoff + jitter.
|
# pull uses bounded retry with exponential backoff + jitter.
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Gem path bootstrap: pin GEM_HOME/GEM_PATH before any require so the script
|
||||||
|
# finds its gems regardless of how it was launched (sudo strips them by
|
||||||
|
# default via env_reset). Derived from this file's own location so the
|
||||||
|
# scripts are relocatable. Guards prevent overriding an explicit environment.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SCRIPT_DIR = File.expand_path(File.dirname(__FILE__))
|
||||||
|
GEMS_DIR = File.join(SCRIPT_DIR, ".gems")
|
||||||
|
ENV["GEM_HOME"] = GEMS_DIR unless ENV["GEM_HOME"]
|
||||||
|
ENV["GEM_PATH"] = GEMS_DIR unless ENV["GEM_PATH"]
|
||||||
|
Gem.paths = { "GEM_HOME" => ENV["GEM_HOME"], "GEM_PATH" => ENV["GEM_PATH"] }
|
||||||
|
|
||||||
require "sequel"
|
require "sequel"
|
||||||
require "net/http"
|
require "net/http"
|
||||||
require "openssl"
|
require "openssl"
|
||||||
|
|
@ -23,7 +35,7 @@ require "base64"
|
||||||
require "securerandom"
|
require "securerandom"
|
||||||
require "rexml/document"
|
require "rexml/document"
|
||||||
|
|
||||||
ROOT = File.expand_path(File.dirname(__FILE__))
|
ROOT = SCRIPT_DIR
|
||||||
CONFIG_PATH = File.join(ROOT, "config.json")
|
CONFIG_PATH = File.join(ROOT, "config.json")
|
||||||
|
|
||||||
AUDIO_EXTS = [".mp3", ".m4a"].freeze
|
AUDIO_EXTS = [".mp3", ".m4a"].freeze
|
||||||
|
|
@ -97,31 +109,43 @@ def release_lock!
|
||||||
end
|
end
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Schema: single source of truth, applied idempotently via Sequel.
|
# Schema: single source of truth, applied idempotently via raw SQL through
|
||||||
|
# Database#execute. Raw DDL avoids the Sequel create_table/alter_table DSL,
|
||||||
|
# which is unreliable under JRuby (instance_exec'd generator methods can
|
||||||
|
# resolve to nil). Works identically on CRuby and JDBC.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
SHOWS_COLUMNS = {
|
SHOWS_SQL = <<~SQL.freeze
|
||||||
slug: { type: :string, primary_key: true },
|
CREATE TABLE IF NOT EXISTS shows (
|
||||||
guid: { type: :string, null: false, unique: true },
|
slug TEXT PRIMARY KEY,
|
||||||
name: { type: :string, null: false },
|
guid TEXT NOT NULL UNIQUE,
|
||||||
feed_url: { type: :string, null: false, unique: true },
|
name TEXT NOT NULL,
|
||||||
source: { type: :string, default: "manual" },
|
feed_url TEXT NOT NULL UNIQUE,
|
||||||
opml_import: { type: :integer, default: 0 },
|
source TEXT DEFAULT 'manual',
|
||||||
archived: { type: :integer, default: 1 },
|
opml_import INTEGER DEFAULT 0,
|
||||||
media_class: { type: :string },
|
archived INTEGER DEFAULT 1,
|
||||||
created_at: { type: :string }
|
media_class TEXT,
|
||||||
}.freeze
|
created_at TEXT
|
||||||
|
);
|
||||||
|
SQL
|
||||||
|
|
||||||
EPISODES_COLUMNS = {
|
EPISODES_SQL = <<~SQL.freeze
|
||||||
id: { type: :integer, primary_key: true, auto_increment: true },
|
CREATE TABLE IF NOT EXISTS episodes (
|
||||||
show_slug: { type: :string, null: false },
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
guid: { type: :string, null: false },
|
show_slug TEXT NOT NULL,
|
||||||
title: { type: :string },
|
guid TEXT NOT NULL,
|
||||||
file_path: { type: :string },
|
title TEXT,
|
||||||
enclosure_url: { type: :string },
|
file_path TEXT,
|
||||||
runlength: { type: :integer },
|
enclosure_url TEXT,
|
||||||
played: { type: :integer, default: 0 },
|
runlength INTEGER,
|
||||||
played_at: { type: :string }
|
played INTEGER DEFAULT 0,
|
||||||
}.freeze
|
played_at TEXT,
|
||||||
|
UNIQUE (show_slug, guid)
|
||||||
|
);
|
||||||
|
SQL
|
||||||
|
|
||||||
|
INDEX_EPISODES_SQL = <<~SQL.freeze
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_episodes_show_played ON episodes (show_slug, played);
|
||||||
|
SQL
|
||||||
|
|
||||||
def tune(db)
|
def tune(db)
|
||||||
# Plain-SQL pragmas via Database#execute, which works on CRuby and JRuby/JDBC
|
# Plain-SQL pragmas via Database#execute, which works on CRuby and JRuby/JDBC
|
||||||
|
|
@ -134,37 +158,18 @@ end
|
||||||
def connect_subs
|
def connect_subs
|
||||||
db = Sequel.connect("jdbc:sqlite:#{$subs_db_path}")
|
db = Sequel.connect("jdbc:sqlite:#{$subs_db_path}")
|
||||||
tune(db)
|
tune(db)
|
||||||
ensure_schema!(db, :shows, SHOWS_COLUMNS)
|
db.execute(SHOWS_SQL)
|
||||||
db
|
db
|
||||||
end
|
end
|
||||||
|
|
||||||
def connect_played
|
def connect_played
|
||||||
db = Sequel.connect("jdbc:sqlite:#{$played_db_path}")
|
db = Sequel.connect("jdbc:sqlite:#{$played_db_path}")
|
||||||
tune(db)
|
tune(db)
|
||||||
ensure_schema!(db, :episodes, EPISODES_COLUMNS)
|
db.execute(EPISODES_SQL)
|
||||||
unless db.index_exists?(:episodes, [:show_slug, :played])
|
db.execute(INDEX_EPISODES_SQL)
|
||||||
db.create_index :episodes, [:show_slug, :played], name: :idx_episodes_show_played
|
|
||||||
end
|
|
||||||
db
|
db
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_schema!(db, table, columns)
|
|
||||||
unless db.table_exists?(table)
|
|
||||||
db.create_table(table) do |t|
|
|
||||||
columns.each do |col, opts|
|
|
||||||
t.column(col, **opts)
|
|
||||||
end
|
|
||||||
t.unique_constraint %i[show_slug guid] if table == :episodes
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
existing = db.columns(table)
|
|
||||||
columns.each do |col, opts|
|
|
||||||
next if existing.include?(col)
|
|
||||||
db.alter_table(table) { |t| t.add_column(col, **opts) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -187,8 +192,8 @@ def classify_feed(entries)
|
||||||
return "audio" if mime.start_with?("audio/")
|
return "audio" if mime.start_with?("audio/")
|
||||||
return "video" if mime.start_with?("video/")
|
return "video" if mime.start_with?("video/")
|
||||||
url = (enclosures.first[:href] || "").downcase
|
url = (enclosures.first[:href] || "").downcase
|
||||||
AUDIO_EXTS.any? { |ext| url.end_with?(ext) } && return "audio"
|
return "audio" if AUDIO_EXTS.any? { |ext| url.end_with?(ext) }
|
||||||
VIDEO_EXTS.any? { |ext| url.end_with?(ext) } && return "video"
|
return "video" if VIDEO_EXTS.any? { |ext| url.end_with?(ext) }
|
||||||
"unknown"
|
"unknown"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,24 @@
|
||||||
# Shares the same exclusive lockfile as fetch_podcasts.rb; skips cleanly if
|
# Shares the same exclusive lockfile as fetch_podcasts.rb; skips cleanly if
|
||||||
# the fetcher holds it. Databases run in WAL mode with a busy timeout.
|
# the fetcher holds it. Databases run in WAL mode with a busy timeout.
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Gem path bootstrap: pin GEM_HOME/GEM_PATH before any require so the script
|
||||||
|
# finds its gems regardless of how it was launched (sudo strips them by
|
||||||
|
# default via env_reset). Derived from this file's own location so the
|
||||||
|
# scripts are relocatable. Guards prevent overriding an explicit environment.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SCRIPT_DIR = File.expand_path(File.dirname(__FILE__))
|
||||||
|
GEMS_DIR = File.join(SCRIPT_DIR, ".gems")
|
||||||
|
ENV["GEM_HOME"] = GEMS_DIR unless ENV["GEM_HOME"]
|
||||||
|
ENV["GEM_PATH"] = GEMS_DIR unless ENV["GEM_PATH"]
|
||||||
|
Gem.paths = { "GEM_HOME" => ENV["GEM_HOME"], "GEM_PATH" => ENV["GEM_PATH"] }
|
||||||
|
|
||||||
require "sequel"
|
require "sequel"
|
||||||
require "json"
|
require "json"
|
||||||
require "logger"
|
require "logger"
|
||||||
require "time"
|
require "time"
|
||||||
|
|
||||||
ROOT = File.expand_path(File.dirname(__FILE__))
|
ROOT = SCRIPT_DIR
|
||||||
CONFIG_PATH = File.join(ROOT, "config.json")
|
CONFIG_PATH = File.join(ROOT, "config.json")
|
||||||
|
|
||||||
$state_dir = nil
|
$state_dir = nil
|
||||||
|
|
@ -52,31 +64,43 @@ def setup_logging!
|
||||||
end
|
end
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Schema: single source of truth, applied idempotently via Sequel.
|
# Schema: single source of truth, applied idempotently via raw SQL through
|
||||||
|
# Database#execute. Raw DDL avoids the Sequel create_table/alter_table DSL,
|
||||||
|
# which is unreliable under JRuby (instance_exec'd generator methods can
|
||||||
|
# resolve to nil). Works identically on CRuby and JDBC.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
SHOWS_COLUMNS = {
|
SHOWS_SQL = <<~SQL.freeze
|
||||||
slug: { type: :string, primary_key: true },
|
CREATE TABLE IF NOT EXISTS shows (
|
||||||
guid: { type: :string, null: false, unique: true },
|
slug TEXT PRIMARY KEY,
|
||||||
name: { type: :string, null: false },
|
guid TEXT NOT NULL UNIQUE,
|
||||||
feed_url: { type: :string, null: false, unique: true },
|
name TEXT NOT NULL,
|
||||||
source: { type: :string, default: "manual" },
|
feed_url TEXT NOT NULL UNIQUE,
|
||||||
opml_import: { type: :integer, default: 0 },
|
source TEXT DEFAULT 'manual',
|
||||||
archived: { type: :integer, default: 1 },
|
opml_import INTEGER DEFAULT 0,
|
||||||
media_class: { type: :string },
|
archived INTEGER DEFAULT 1,
|
||||||
created_at: { type: :string }
|
media_class TEXT,
|
||||||
}.freeze
|
created_at TEXT
|
||||||
|
);
|
||||||
|
SQL
|
||||||
|
|
||||||
EPISODES_COLUMNS = {
|
EPISODES_SQL = <<~SQL.freeze
|
||||||
id: { type: :integer, primary_key: true, auto_increment: true },
|
CREATE TABLE IF NOT EXISTS episodes (
|
||||||
show_slug: { type: :string, null: false },
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
guid: { type: :string, null: false },
|
show_slug TEXT NOT NULL,
|
||||||
title: { type: :string },
|
guid TEXT NOT NULL,
|
||||||
file_path: { type: :string },
|
title TEXT,
|
||||||
enclosure_url: { type: :string },
|
file_path TEXT,
|
||||||
runlength: { type: :integer },
|
enclosure_url TEXT,
|
||||||
played: { type: :integer, default: 0 },
|
runlength INTEGER,
|
||||||
played_at: { type: :string }
|
played INTEGER DEFAULT 0,
|
||||||
}.freeze
|
played_at TEXT,
|
||||||
|
UNIQUE (show_slug, guid)
|
||||||
|
);
|
||||||
|
SQL
|
||||||
|
|
||||||
|
INDEX_EPISODES_SQL = <<~SQL.freeze
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_episodes_show_played ON episodes (show_slug, played);
|
||||||
|
SQL
|
||||||
|
|
||||||
def tune(db)
|
def tune(db)
|
||||||
# Plain-SQL pragmas via Database#execute, which works on CRuby and JRuby/JDBC
|
# Plain-SQL pragmas via Database#execute, which works on CRuby and JRuby/JDBC
|
||||||
|
|
@ -89,37 +113,18 @@ end
|
||||||
def connect_subs
|
def connect_subs
|
||||||
db = Sequel.connect("jdbc:sqlite:#{$subs_db_path}")
|
db = Sequel.connect("jdbc:sqlite:#{$subs_db_path}")
|
||||||
tune(db)
|
tune(db)
|
||||||
ensure_schema!(db, :shows, SHOWS_COLUMNS)
|
db.execute(SHOWS_SQL)
|
||||||
db
|
db
|
||||||
end
|
end
|
||||||
|
|
||||||
def connect_played
|
def connect_played
|
||||||
db = Sequel.connect("jdbc:sqlite:#{$played_db_path}")
|
db = Sequel.connect("jdbc:sqlite:#{$played_db_path}")
|
||||||
tune(db)
|
tune(db)
|
||||||
ensure_schema!(db, :episodes, EPISODES_COLUMNS)
|
db.execute(EPISODES_SQL)
|
||||||
unless db.index_exists?(:episodes, [:show_slug, :played])
|
db.execute(INDEX_EPISODES_SQL)
|
||||||
db.create_index :episodes, [:show_slug, :played], name: :idx_episodes_show_played
|
|
||||||
end
|
|
||||||
db
|
db
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_schema!(db, table, columns)
|
|
||||||
unless db.table_exists?(table)
|
|
||||||
db.create_table(table) do |t|
|
|
||||||
columns.each do |col, opts|
|
|
||||||
t.column(col, **opts)
|
|
||||||
end
|
|
||||||
t.unique_constraint %i[show_slug guid] if table == :episodes
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
existing = db.columns(table)
|
|
||||||
columns.each do |col, opts|
|
|
||||||
next if existing.include?(col)
|
|
||||||
db.alter_table(table) { |t| t.add_column(col, **opts) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Mutual exclusion via flock on a shared lockfile.
|
# Mutual exclusion via flock on a shared lockfile.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue