mirror of
https://github.com/mistergibson/radio.git
synced 2026-09-08 22:09:51 -07:00
219 lines
6.1 KiB
Ruby
Executable file
219 lines
6.1 KiB
Ruby
Executable file
#!/usr/bin/env jruby
|
|
# frozen_string_literal: true
|
|
|
|
require "json"
|
|
require "jdbc/sqlite3"
|
|
require "fileutils"
|
|
require "optparse"
|
|
|
|
Jdbc::SQLite3.load_driver
|
|
|
|
module RadioAutomation
|
|
ROOT = File.expand_path("..", __dir__)
|
|
CONFIG_PATH = File.join(ROOT, "config.json")
|
|
|
|
STORAGE_DIR = nil
|
|
STATE_DIR = nil
|
|
SUBS_DB = nil
|
|
PLAYED_DB = nil
|
|
PODCASTS_DIR = nil
|
|
PLAYLISTS_DIR = nil
|
|
LOGS_DIR = nil
|
|
|
|
def self.init_paths
|
|
cfg = JSON.parse(File.read(CONFIG_PATH))
|
|
@storage = File.expand_path(cfg["storage"])
|
|
self.STORAGE_DIR = @storage
|
|
self.STATE_DIR = File.join(@storage, "state")
|
|
self.SUBS_DB = File.join(STATE_DIR, "subscriptions.db")
|
|
self.PLAYED_DB = File.join(STATE_DIR, "played.db")
|
|
self.PODCASTS_DIR = File.join(@storage, "podcasts")
|
|
self.PLAYLISTS_DIR = File.join(@storage, "playlists")
|
|
self.LOGS_DIR = File.join(@storage, "logs")
|
|
end
|
|
|
|
# --- JDBC connection helpers ---------------------------------------------
|
|
|
|
def self.jdb_connect(db_file)
|
|
java.sql.DriverManager.getConnection("jdbc:sqlite:#{db_file}")
|
|
end
|
|
|
|
def self.jdb_query(conn, sql, params = [])
|
|
stmt = conn.prepareStatement(sql)
|
|
params.each_with_index { |p, i| stmt.setObject(i + 1, p) }
|
|
rs = stmt.executeQuery
|
|
cols = []
|
|
meta = rs.getMetaData
|
|
(1..meta.getColumnCount).each { |i| cols << meta.getColumnName(i) }
|
|
rows = []
|
|
while rs.next
|
|
row = {}
|
|
cols.each { |c| row[c] = rs.getObject(c) }
|
|
rows << row
|
|
end
|
|
rs.close
|
|
stmt.close
|
|
rows
|
|
end
|
|
|
|
def self.jdb_exec(conn, sql, params = [])
|
|
stmt = conn.prepareStatement(sql)
|
|
params.each_with_index { |p, i| stmt.setObject(i + 1, p) }
|
|
stmt.executeUpdate
|
|
stmt.close
|
|
end
|
|
|
|
def self.log_info(msg)
|
|
puts "#{Time.now.iso8601} [INFO] #{msg}"
|
|
append_log("update.log", msg)
|
|
end
|
|
|
|
def self.append_log(filename, msg)
|
|
FileUtils.mkdir_p(LOGS_DIR)
|
|
File.open(File.join(LOGS_DIR, filename), "a") { |f| f.puts msg }
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
|
|
def self.open_subs_db
|
|
db = jdb_connect(SUBS_DB)
|
|
jdb_exec(db, <<-SQL)
|
|
CREATE TABLE IF NOT EXISTS shows (
|
|
slug TEXT PRIMARY KEY,
|
|
guid TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
feed_url TEXT NOT NULL UNIQUE,
|
|
source TEXT DEFAULT 'manual',
|
|
opml_import INTEGER DEFAULT 0,
|
|
archived INTEGER DEFAULT 1,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
)
|
|
SQL
|
|
db
|
|
end
|
|
|
|
def self.open_played_db
|
|
db = jdb_connect(PLAYED_DB)
|
|
jdb_exec(db, <<-SQL)
|
|
CREATE TABLE IF NOT EXISTS episodes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
show_slug TEXT NOT NULL,
|
|
guid TEXT NOT NULL,
|
|
title TEXT,
|
|
file_path TEXT,
|
|
enclosure_url TEXT,
|
|
runlength INTEGER,
|
|
played INTEGER DEFAULT 0,
|
|
played_at TEXT,
|
|
UNIQUE(show_slug, guid)
|
|
)
|
|
SQL
|
|
db
|
|
end
|
|
|
|
# Pick the next unplayed episode for a show, keyed by guid.
|
|
# Archived: prefer a local file_path; Live: use enclosure_url.
|
|
def self.select_unplayed_episode(slug, played_db)
|
|
rows = jdb_query(
|
|
played_db,
|
|
"SELECT guid, title, file_path, enclosure_url, runlength FROM episodes WHERE show_slug = ? AND played = 0 ORDER BY id ASC LIMIT 1",
|
|
[slug]
|
|
)
|
|
return nil if rows.empty?
|
|
rows.first
|
|
end
|
|
|
|
# Build the annotated URI line station.liq consumes.
|
|
# Archived -> local file path; Live -> remote enclosure URL.
|
|
def self.annotated_uri(ep)
|
|
uri = ep["file_path"] || ep["enclosure_url"]
|
|
return nil if uri.nil? || uri.to_s.empty?
|
|
rl = ep["runlength"].to_i
|
|
title = ep["title"].to_s.gsub('"', "'")
|
|
"annotate:liq_runlength=\"#{rl}\",liq_title=\"#{title}\":#{uri}"
|
|
end
|
|
|
|
def self.write_queue_line(slug, line, out_path)
|
|
FileUtils.mkdir_p(File.dirname(out_path))
|
|
File.open(out_path, "w") { |f| f.puts(line) }
|
|
end
|
|
|
|
def self.mark_as_played(slug, guid, played_db)
|
|
jdb_exec(
|
|
played_db,
|
|
"UPDATE episodes SET played = 1, played_at = datetime('now') WHERE show_slug = ? AND guid = ?",
|
|
[slug, guid]
|
|
)
|
|
end
|
|
|
|
def self.update_all
|
|
subs_db = open_subs_db
|
|
played_db = open_played_db
|
|
shows = jdb_query(subs_db, "SELECT slug, name, archived FROM shows ORDER BY name")
|
|
|
|
queued = 0
|
|
skipped = 0
|
|
shows.each do |show|
|
|
slug = show["slug"]
|
|
ep = select_unplayed_episode(slug, played_db)
|
|
if ep.nil?
|
|
log_info("#{slug}: no unplayed episodes, skipping.")
|
|
skipped += 1
|
|
next
|
|
end
|
|
line = annotated_uri(ep)
|
|
if line.nil?
|
|
log_info("#{slug}: episode #{ep['guid']} has no usable URI, skipping.")
|
|
skipped += 1
|
|
next
|
|
end
|
|
out_pls = File.join(PLAYLISTS_DIR, "#{slug}.txt")
|
|
write_queue_line(slug, line, out_pls)
|
|
mark_as_played(slug, ep["guid"], played_db)
|
|
kind = ep["file_path"] ? "downloaded" : "live"
|
|
log_info("#{slug}: queued #{ep['title']} [#{kind}] runlength=#{ep['runlength'].to_i}s")
|
|
queued += 1
|
|
end
|
|
|
|
subs_db.close
|
|
played_db.close
|
|
log_info("=== Update complete: #{queued} queued, #{skipped} skipped ===")
|
|
end
|
|
|
|
def self.json_summary
|
|
subs_db = open_subs_db
|
|
played_db = open_played_db
|
|
shows = jdb_query(subs_db, "SELECT slug FROM shows ORDER BY name")
|
|
summary = {}
|
|
shows.each do |show|
|
|
slug = show["slug"]
|
|
total = jdb_query(played_db, "SELECT COUNT(*) AS c FROM episodes WHERE show_slug = ?", [slug]).first["c"].to_i
|
|
played = jdb_query(played_db, "SELECT COUNT(*) AS c FROM episodes WHERE show_slug = ? AND played = 1", [slug]).first["c"].to_i
|
|
summary[slug] = { "total_episodes" => total, "played_count" => played, "unplayed" => total - played }
|
|
end
|
|
subs_db.close
|
|
played_db.close
|
|
puts JSON.pretty_generate(summary)
|
|
end
|
|
|
|
def self.main
|
|
options = {}
|
|
OptionParser.new do |opts|
|
|
opts.banner = "Usage: update_playlists.rb [options]"
|
|
opts.on("--json", "Emit JSON summary and exit") { options[:json] = true }
|
|
end.parse!
|
|
|
|
init_paths
|
|
FileUtils.mkdir_p(STATE_DIR)
|
|
FileUtils.mkdir_p(LOGS_DIR)
|
|
FileUtils.mkdir_p(PLAYLISTS_DIR)
|
|
|
|
if options[:json]
|
|
json_summary
|
|
else
|
|
update_all
|
|
end
|
|
end
|
|
end
|
|
|
|
RadioAutomation.main
|