mirror of
https://github.com/mistergibson/radio.git
synced 2026-09-08 22:09:51 -07:00
Overhaul
This commit is contained in:
parent
986929cb4e
commit
04098bc151
6 changed files with 385 additions and 187 deletions
|
|
@ -2,14 +2,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "json"
|
||||
require "sqlite3"
|
||||
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")
|
||||
AUDIO_EXTS = [".mp3", ".m4a"]
|
||||
|
||||
STORAGE_DIR = nil
|
||||
STATE_DIR = nil
|
||||
|
|
@ -31,6 +32,37 @@ module RadioAutomation
|
|||
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)
|
||||
|
|
@ -44,22 +76,34 @@ module RadioAutomation
|
|||
end
|
||||
|
||||
def self.open_subs_db
|
||||
db = SQLite3::Database.new(SUBS_DB)
|
||||
db.results_as_hash = true
|
||||
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 = SQLite3::Database.new(PLAYED_DB)
|
||||
db.results_as_hash = true
|
||||
db.execute <<-SQL
|
||||
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,
|
||||
duration_seconds INTEGER,
|
||||
enclosure_url TEXT,
|
||||
runlength INTEGER,
|
||||
played INTEGER DEFAULT 0,
|
||||
played_at TEXT,
|
||||
UNIQUE(show_slug, guid)
|
||||
)
|
||||
|
|
@ -67,80 +111,85 @@ module RadioAutomation
|
|||
db
|
||||
end
|
||||
|
||||
def self.find_audio_files(directory)
|
||||
return [] unless Dir.exist?(directory)
|
||||
Dir.glob(File.join(directory, "**", "*")).select do |f|
|
||||
File.file?(f) && AUDIO_EXTS.any? { |ext| f.end_with?(ext) }
|
||||
end.sort
|
||||
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)
|
||||
files = find_audio_files(File.join(PODCASTS_DIR, slug))
|
||||
return nil if files.empty?
|
||||
|
||||
played_rows = played_db.query_all(
|
||||
"SELECT file_path, played_at FROM episodes WHERE show_slug = ? AND played_at IS NOT NULL", slug
|
||||
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]
|
||||
)
|
||||
played_map = played_rows.each_with_object({}) { |r, h| h[r["file_path"]] = r["played_at"] }
|
||||
|
||||
unplayed = files.reject { |f| played_map.key?(f) }
|
||||
return unplayed.first if unplayed.any?
|
||||
|
||||
if played_map.any?
|
||||
played_map.min_by { |_path, ts| ts.to_s }[0]
|
||||
else
|
||||
files.first
|
||||
end
|
||||
return nil if rows.empty?
|
||||
rows.first
|
||||
end
|
||||
|
||||
def self.write_pls(filepath, out_path)
|
||||
abs = File.absolute_path(filepath)
|
||||
content = "[playlist]\nFile1=#{abs}\nTitle1=Radio Episode\nLength1=-1\nNumberOfEntries=1\nVersion=2\n"
|
||||
# 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.write(out_path, content)
|
||||
File.open(out_path, "w") { |f| f.puts(line) }
|
||||
end
|
||||
|
||||
def self.mark_as_played(slug, filepath, played_db)
|
||||
played_db.execute(
|
||||
"UPDATE episodes SET played_at = datetime('now') WHERE show_slug = ? AND file_path = ?",
|
||||
[slug, filepath]
|
||||
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 = subs_db.query_all("SELECT slug, name FROM shows ORDER BY name")
|
||||
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"]
|
||||
selected = select_unplayed_episode(slug, played_db)
|
||||
if selected.nil?
|
||||
log_info("#{slug}: no audio files found, skipping.")
|
||||
ep = select_unplayed_episode(slug, played_db)
|
||||
if ep.nil?
|
||||
log_info("#{slug}: no unplayed episodes, skipping.")
|
||||
skipped += 1
|
||||
next
|
||||
end
|
||||
out_pls = File.join(PLAYLISTS_DIR, "#{slug}.pls")
|
||||
write_pls(selected, out_pls)
|
||||
mark_as_played(slug, selected, played_db)
|
||||
log_info("#{slug}: queued #{File.basename(selected)}")
|
||||
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 = subs_db.query_all("SELECT slug FROM shows ORDER BY name")
|
||||
shows = jdb_query(subs_db, "SELECT slug FROM shows ORDER BY name")
|
||||
summary = {}
|
||||
shows.each do |show|
|
||||
slug = show["slug"]
|
||||
files = find_audio_files(File.join(PODCASTS_DIR, slug))
|
||||
played_count = played_db.get_first_value(
|
||||
"SELECT COUNT(*) FROM episodes WHERE show_slug = ? AND played_at IS NOT NULL", slug
|
||||
)
|
||||
summary[slug] = { "total_files" => files.size, "played_count" => played_count }
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue