mirror of
https://github.com/mistergibson/radio.git
synced 2026-09-08 22:09:51 -07:00
162 lines
4.9 KiB
Python
Executable file
162 lines
4.9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
update_playlists.py - Regenerate per-show .pls playlists based on playback history.
|
|
Runs via cron hourly at :30.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import re
|
|
import sqlite3
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
STATE_DIR = ROOT / "state"
|
|
SUBS_DB = STATE_DIR / "subscriptions.db"
|
|
PLAYED_DB = STATE_DIR / "played.db"
|
|
PODCASTS_DIR = ROOT / "podcasts"
|
|
PLAYLISTS_DIR = ROOT / "playlists"
|
|
LOGS_DIR = ROOT / "logs"
|
|
|
|
AUDIO_EXTS = {".mp3", ".m4a"}
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
handlers=[
|
|
logging.FileHandler(LOGS_DIR / "update.log"),
|
|
logging.StreamHandler(sys.stdout),
|
|
],
|
|
)
|
|
log = logging.getLogger("update_playlists")
|
|
|
|
def open_subs_db():
|
|
conn = sqlite3.connect(SUBS_DB)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
def open_played_db():
|
|
conn = sqlite3.connect(PLAYED_DB)
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("""
|
|
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,
|
|
played_at TEXT,
|
|
UNIQUE(show_slug, guid)
|
|
)
|
|
""")
|
|
conn.commit()
|
|
return conn
|
|
|
|
def find_audio_files(directory):
|
|
"""Recursively scan for .mp3/.m4a files."""
|
|
results = []
|
|
if not directory.exists():
|
|
return results
|
|
for p in sorted(directory.rglob("*")):
|
|
if p.is_file() and p.suffix.lower() in AUDIO_EXTS:
|
|
results.append(str(p))
|
|
return results
|
|
|
|
def select_unplayed_episode(slug, played_db):
|
|
"""Pick one unplayed episode for the show, falling back to least-recently-played."""
|
|
files = find_audio_files(PODCASTS_DIR / slug)
|
|
if not files:
|
|
return None
|
|
|
|
played_rows = played_db.execute(
|
|
"SELECT file_path, played_at FROM episodes WHERE show_slug = ? AND played_at IS NOT NULL",
|
|
(slug,),
|
|
).fetchall()
|
|
played_paths = {row["file_path"]: row["played_at"] for row in played_rows}
|
|
|
|
unplayed = [f for f in files if f not in played_paths]
|
|
if unplayed:
|
|
return unplayed[0]
|
|
|
|
# All played: pick least recently played
|
|
if played_paths:
|
|
return min(played_paths.items(), key=lambda kv: (kv[1] or ""))[0]
|
|
return files[0]
|
|
|
|
def write_pls(filepath, out_path):
|
|
"""Write a .pls playlist pointing at a single file."""
|
|
abs_path = str(Path(filepath).resolve())
|
|
content = f"[playlist]\nFile1={abs_path}\nTitle1=Radio Episode\nLength1=-1\nNumberOfEntries=1\nVersion=2\n"
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
out_path.write_text(content)
|
|
|
|
def mark_as_played(slug, filepath, duration, played_db):
|
|
played_db.execute(
|
|
"UPDATE episodes SET played_at = datetime('now') WHERE show_slug = ? AND file_path = ?",
|
|
(slug, filepath),
|
|
)
|
|
played_db.commit()
|
|
|
|
def update_all():
|
|
subs_db = open_subs_db()
|
|
played_db = open_played_db()
|
|
shows = subs_db.execute("SELECT slug, name FROM shows ORDER BY name").fetchall()
|
|
|
|
for show in shows:
|
|
slug = show["slug"]
|
|
selected = select_unplayed_episode(slug, played_db)
|
|
if selected is None:
|
|
log.info("%s: no audio files found, skipping.", slug)
|
|
continue
|
|
|
|
duration_row = played_db.execute(
|
|
"SELECT duration_seconds FROM episodes WHERE show_slug = ? AND file_path = ?",
|
|
(slug, selected),
|
|
).fetchone()
|
|
duration = duration_row["duration_seconds"] if duration_row else None
|
|
|
|
out_pls = PLAYLISTS_DIR / f"{slug}.pls"
|
|
write_pls(selected, out_pls)
|
|
mark_as_played(slug, selected, duration, played_db)
|
|
log.info("%s: queued %s", slug, Path(selected).name)
|
|
|
|
subs_db.close()
|
|
played_db.close()
|
|
|
|
def json_summary():
|
|
subs_db = open_subs_db()
|
|
played_db = open_played_db()
|
|
shows = subs_db.execute("SELECT slug FROM shows ORDER BY name").fetchall()
|
|
summary = {}
|
|
for show in shows:
|
|
slug = show["slug"]
|
|
files = find_audio_files(PODCASTS_DIR / slug)
|
|
played_count = played_db.execute(
|
|
"SELECT COUNT(*) as c FROM episodes WHERE show_slug = ? AND played_at IS NOT NULL",
|
|
(slug,),
|
|
).fetchone()["c"]
|
|
summary[slug] = {"total_files": len(files), "played_count": played_count}
|
|
subs_db.close()
|
|
played_db.close()
|
|
print(json.dumps(summary, indent=2))
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Playlist updater for radio automation")
|
|
parser.add_argument("--json", action="store_true", help="Emit JSON summary and exit")
|
|
args = parser.parse_args()
|
|
|
|
STATE_DIR.mkdir(exist_ok=True)
|
|
LOGS_DIR.mkdir(exist_ok=True)
|
|
PLAYLISTS_DIR.mkdir(exist_ok=True)
|
|
|
|
if args.json:
|
|
json_summary()
|
|
else:
|
|
update_all()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|