3 files changed
+157
-51
No files matched your search
@@ -23,6 +23,8 @@ DEFAULT_CONFIG = {
|
|||||||
NON_SECRET_KEYS = {
|
NON_SECRET_KEYS = {
|
||||||
"SERVER_IP",
|
"SERVER_IP",
|
||||||
"PLAYLIST_NAME",
|
"PLAYLIST_NAME",
|
||||||
|
"PLAYLIST_NAMES",
|
||||||
|
"PLAYLISTS",
|
||||||
"AUTO_UPDATE",
|
"AUTO_UPDATE",
|
||||||
"UPDATE_INTERVAL",
|
"UPDATE_INTERVAL",
|
||||||
"UPDATE_UNIT",
|
"UPDATE_UNIT",
|
||||||
|
|||||||
@@ -100,10 +100,35 @@ def run_update_job(config=None, dry_run=False, persist_history=True, write_buy_l
|
|||||||
"skipped_songs": [],
|
"skipped_songs": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
# station_key → display name used in song["source"]
|
||||||
|
STATION_SOURCE = {"journey_fm": "Journey FM", "spirit_fm": "Spirit FM", "klove": "K-LOVE"}
|
||||||
|
|
||||||
token = config.get("PLEX_TOKEN", "").strip()
|
token = config.get("PLEX_TOKEN", "").strip()
|
||||||
server_ip = config.get("SERVER_IP", "").strip()
|
server_ip = config.get("SERVER_IP", "").strip()
|
||||||
playlist_name = config.get("PLAYLIST_NAME", "Journey FM Recently Played").strip()
|
|
||||||
selected_stations = config.get("SELECTED_STATIONS", ["journey_fm", "spirit_fm"])
|
# Build playlist configs: [{name, stations}]
|
||||||
|
playlists_raw = config.get("PLAYLISTS")
|
||||||
|
if playlists_raw:
|
||||||
|
try:
|
||||||
|
playlists = _json.loads(playlists_raw) if isinstance(playlists_raw, str) else playlists_raw
|
||||||
|
except Exception:
|
||||||
|
playlists = []
|
||||||
|
else:
|
||||||
|
playlists = []
|
||||||
|
|
||||||
|
if not playlists:
|
||||||
|
# Fall back to flat PLAYLIST_NAMES / PLAYLIST_NAME + SELECTED_STATIONS
|
||||||
|
raw_names = config.get("PLAYLIST_NAMES") or config.get("PLAYLIST_NAME", "Journey FM Recently Played")
|
||||||
|
if isinstance(raw_names, list):
|
||||||
|
names = raw_names
|
||||||
|
else:
|
||||||
|
names = [n.strip() for n in str(raw_names).split(",") if n.strip()]
|
||||||
|
stations = config.get("SELECTED_STATIONS", ["journey_fm"])
|
||||||
|
if isinstance(stations, str):
|
||||||
|
stations = [s.strip() for s in stations.split(",") if s.strip()]
|
||||||
|
playlists = [{"name": n, "stations": list(stations)} for n in (names or ["Journey FM Recently Played"])]
|
||||||
|
|
||||||
if not token or not server_ip:
|
if not token or not server_ip:
|
||||||
result["status"] = "error"
|
result["status"] = "error"
|
||||||
@@ -113,17 +138,28 @@ def run_update_job(config=None, dry_run=False, persist_history=True, write_buy_l
|
|||||||
if persist_history:
|
if persist_history:
|
||||||
init_history_db()
|
init_history_db()
|
||||||
|
|
||||||
scrape_result = scrape_recently_played(selected_stations)
|
# Scrape the union of all stations across all playlists (no duplicate scrapes)
|
||||||
songs = scrape_result["songs"]
|
all_station_keys = list({s for pl in playlists for s in pl.get("stations", [])})
|
||||||
|
scrape_result = scrape_recently_played(all_station_keys)
|
||||||
|
all_songs = scrape_result["songs"]
|
||||||
result["station_breakdown"] = scrape_result["station_results"]
|
result["station_breakdown"] = scrape_result["station_results"]
|
||||||
result["scraped_count"] = len(songs)
|
result["scraped_count"] = len(all_songs)
|
||||||
# Keep scraped songs for history analytics (song play counts per station)
|
result["scraped_songs"] = all_songs
|
||||||
result["scraped_songs"] = songs
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
plex = connect_to_plex_server(token, server_ip)
|
plex = connect_to_plex_server(token, server_ip)
|
||||||
playlist_result = create_or_update_playlist(plex, songs, playlist_name, dry_run=dry_run)
|
merged = {}
|
||||||
result.update(playlist_result)
|
for pl in playlists:
|
||||||
|
pl_sources = {STATION_SOURCE.get(s, s) for s in pl.get("stations", [])}
|
||||||
|
pl_songs = [s for s in all_songs if s.get("source", "") in pl_sources] if pl_sources else all_songs
|
||||||
|
pr = create_or_update_playlist(plex, pl_songs, pl["name"], dry_run=dry_run)
|
||||||
|
if not merged:
|
||||||
|
merged = pr
|
||||||
|
else:
|
||||||
|
merged["added_count"] = merged.get("added_count", 0) + pr.get("added_count", 0)
|
||||||
|
merged["added_songs"] = merged.get("added_songs", []) + pr.get("added_songs", [])
|
||||||
|
merged["duplicate_count"] = merged.get("duplicate_count", 0) + pr.get("duplicate_count", 0)
|
||||||
|
result.update(merged)
|
||||||
result["missing_count"] = len(result.get("missing_songs", []))
|
result["missing_count"] = len(result.get("missing_songs", []))
|
||||||
result["skipped_count"] = len(result.get("skipped_songs", []))
|
result["skipped_count"] = len(result.get("skipped_songs", []))
|
||||||
if write_buy_list and not dry_run:
|
if write_buy_list and not dry_run:
|
||||||
|
|||||||
+110
-42
@@ -186,25 +186,50 @@ def dedup_buy_list(buy_list_path=None):
|
|||||||
# ── Config ────────────────────────────────────────────────────
|
# ── Config ────────────────────────────────────────────────────
|
||||||
|
|
||||||
def get_web_config():
|
def get_web_config():
|
||||||
config = load_runtime_config()
|
from journeyfm.config_store import _read_config_file, DEFAULT_CONFIG, _normalize_selected_stations
|
||||||
token = config.get("PLEX_TOKEN", "")
|
# Read config.json directly so the form is consistent in container environments
|
||||||
config["_has_token"] = bool(token)
|
# where env vars override the runtime config (env vars still win at sync time).
|
||||||
|
file_cfg = _read_config_file()
|
||||||
|
config = dict(DEFAULT_CONFIG)
|
||||||
|
config.update({k: v for k, v in file_cfg.items() if k != "PLEX_TOKEN"})
|
||||||
|
config["_has_token"] = bool(get_secret("PLEX_TOKEN"))
|
||||||
config["PLEX_TOKEN"] = ""
|
config["PLEX_TOKEN"] = ""
|
||||||
stations = config.get("SELECTED_STATIONS", [])
|
# Parse PLAYLISTS JSON → list of {name, stations}
|
||||||
if isinstance(stations, str):
|
playlists_raw = config.get("PLAYLISTS")
|
||||||
stations = [s.strip() for s in stations.split(",") if s.strip()]
|
if playlists_raw:
|
||||||
config["SELECTED_STATIONS"] = stations
|
try:
|
||||||
|
playlists = json.loads(playlists_raw) if isinstance(playlists_raw, str) else playlists_raw
|
||||||
|
except Exception:
|
||||||
|
playlists = []
|
||||||
|
else:
|
||||||
|
playlists = []
|
||||||
|
if not playlists:
|
||||||
|
# Build from flat keys for backward compat
|
||||||
|
raw_names = config.get("PLAYLIST_NAMES") or config.get("PLAYLIST_NAME", "")
|
||||||
|
if isinstance(raw_names, list):
|
||||||
|
names = raw_names
|
||||||
|
else:
|
||||||
|
names = [n.strip() for n in str(raw_names).split(",") if n.strip()]
|
||||||
|
stations = list(_normalize_selected_stations(config.get("SELECTED_STATIONS", [])))
|
||||||
|
playlists = [{"name": n, "stations": stations} for n in names] if names else []
|
||||||
|
config["PLAYLISTS"] = playlists
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def save_web_config(data):
|
def save_web_config(data):
|
||||||
config = {}
|
config = {}
|
||||||
config["SERVER_IP"] = str(data.get("SERVER_IP", "")).strip()
|
config["SERVER_IP"] = str(data.get("SERVER_IP", "")).strip()
|
||||||
config["PLAYLIST_NAME"] = str(data.get("PLAYLIST_NAME", "")).strip()
|
# PLAYLISTS: list of {name, stations}
|
||||||
stations = data.get("SELECTED_STATIONS", [])
|
playlists = data.get("PLAYLISTS", [])
|
||||||
if isinstance(stations, str):
|
if isinstance(playlists, list):
|
||||||
stations = [s.strip() for s in stations.split(",") if s.strip()]
|
clean = [{"name": str(p.get("name", "")).strip(), "stations": list(p.get("stations", []))}
|
||||||
config["SELECTED_STATIONS"] = stations
|
for p in playlists if str(p.get("name", "")).strip()]
|
||||||
|
else:
|
||||||
|
clean = []
|
||||||
|
config["PLAYLISTS"] = json.dumps(clean)
|
||||||
|
config["PLAYLIST_NAME"] = clean[0]["name"] if clean else ""
|
||||||
|
all_stations = list({s for p in clean for s in p.get("stations", [])})
|
||||||
|
config["SELECTED_STATIONS"] = all_stations
|
||||||
config["AUTO_UPDATE"] = bool(data.get("AUTO_UPDATE", False))
|
config["AUTO_UPDATE"] = bool(data.get("AUTO_UPDATE", False))
|
||||||
try:
|
try:
|
||||||
config["UPDATE_INTERVAL"] = int(data.get("UPDATE_INTERVAL", 15))
|
config["UPDATE_INTERVAL"] = int(data.get("UPDATE_INTERVAL", 15))
|
||||||
@@ -218,11 +243,14 @@ def save_web_config(data):
|
|||||||
|
|
||||||
# ── Playlist ──────────────────────────────────────────────────
|
# ── Playlist ──────────────────────────────────────────────────
|
||||||
|
|
||||||
def get_playlist_tracks():
|
def get_playlist_tracks(name=None):
|
||||||
from journeyfm.plex_service import connect_to_plex_server
|
from journeyfm.plex_service import connect_to_plex_server
|
||||||
config = load_runtime_config()
|
config = load_runtime_config()
|
||||||
plex = connect_to_plex_server(config.get("PLEX_TOKEN", ""), config.get("SERVER_IP", ""))
|
plex = connect_to_plex_server(config.get("PLEX_TOKEN", ""), config.get("SERVER_IP", ""))
|
||||||
name = config.get("PLAYLIST_NAME", "")
|
if not name:
|
||||||
|
cfg = get_web_config()
|
||||||
|
names = cfg.get("PLAYLIST_NAMES") or []
|
||||||
|
name = names[0] if names else config.get("PLAYLIST_NAME", "")
|
||||||
playlist = plex.playlist(name)
|
playlist = plex.playlist(name)
|
||||||
tracks = []
|
tracks = []
|
||||||
for item in playlist.items():
|
for item in playlist.items():
|
||||||
@@ -538,6 +566,7 @@ tr:last-child td{border-bottom:none}tr:hover td{background:var(--sf2)}
|
|||||||
<div class="th">
|
<div class="th">
|
||||||
<span class="tht" id="pl-ttl">Playlist</span>
|
<span class="tht" id="pl-ttl">Playlist</span>
|
||||||
<span class="mono cm" id="pl-ct" style="font-size:.74rem"></span>
|
<span class="mono cm" id="pl-ct" style="font-size:.74rem"></span>
|
||||||
|
<select class="fin" id="pl-sel" onchange="loadPlaylist()" style="max-width:200px;display:none"></select>
|
||||||
<input class="fin" type="search" id="plq" placeholder="Search…" oninput="filterPL()">
|
<input class="fin" type="search" id="plq" placeholder="Search…" oninput="filterPL()">
|
||||||
<button class="btn bts" id="pl-btn" onclick="loadPlaylist()" style="font-size:.78rem">
|
<button class="btn bts" id="pl-btn" onclick="loadPlaylist()" style="font-size:.78rem">
|
||||||
<span class="bl">↺ Load</span><span class="spin"></span>
|
<span class="bl">↺ Load</span><span class="spin"></span>
|
||||||
@@ -578,22 +607,11 @@ tr:last-child td{border-bottom:none}tr:hover td{background:var(--sf2)}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sgrp">
|
<div class="sgrp">
|
||||||
<div class="sgh">Playlist</div>
|
<div class="sgh">Playlists & Stations</div>
|
||||||
<div class="sgb">
|
<div class="sgb">
|
||||||
<div class="fl">
|
<div class="fl-h" style="margin-bottom:.5rem">Each playlist syncs songs from its own set of stations.</div>
|
||||||
<label class="fl-l" for="f-pl">Playlist Name</label>
|
<div id="pl-list" style="display:flex;flex-direction:column;gap:.7rem"></div>
|
||||||
<input class="fi" id="f-pl" type="text" placeholder="Journey FM Recently Played">
|
<button class="btn btg" style="align-self:flex-start;margin-top:.3rem;font-size:.8rem" onclick="addPlaylistRow({name:'',stations:[]})">+ Add Playlist</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="sgrp">
|
|
||||||
<div class="sgh">Stations</div>
|
|
||||||
<div class="sgb">
|
|
||||||
<div class="stc">
|
|
||||||
<label class="ck"><input type="checkbox" id="st-j" value="journey_fm"><span>Journey FM</span><span class="ck-m">myjourneyfm.com</span></label>
|
|
||||||
<label class="ck"><input type="checkbox" id="st-s" value="spirit_fm"><span>Spirit FM</span><span class="ck-m">spiritfm.com</span></label>
|
|
||||||
<label class="ck"><input type="checkbox" id="st-k" value="klove"><span>K-LOVE</span><span class="ck-m">klove.com</span></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sgrp">
|
<div class="sgrp">
|
||||||
@@ -658,6 +676,7 @@ function go(s){
|
|||||||
if(s==='songs'&&!songsReady&&statsData)buildSongs();
|
if(s==='songs'&&!songsReady&&statsData)buildSongs();
|
||||||
if(s==='history'&&!histReady)loadHist();
|
if(s==='history'&&!histReady)loadHist();
|
||||||
if(s==='buylist'&&!buyReady)loadBuy();
|
if(s==='buylist'&&!buyReady)loadBuy();
|
||||||
|
if(s==='playlist')initPlaylistSel();
|
||||||
if(s==='settings')loadCfg();
|
if(s==='settings')loadCfg();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,9 +906,21 @@ function copyAllLinks(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Playlist ──────────────────────────────────────────────────
|
// ── Playlist ──────────────────────────────────────────────────
|
||||||
|
function initPlaylistSel(){
|
||||||
|
fetch('/api/config').then(r=>r.json()).then(cfg=>{
|
||||||
|
const pls=cfg.PLAYLISTS||[];
|
||||||
|
const sel=document.getElementById('pl-sel');
|
||||||
|
if(pls.length>1){
|
||||||
|
sel.innerHTML=pls.map(p=>`<option value="${esc(p.name)}">${esc(p.name)}</option>`).join('');
|
||||||
|
sel.style.display='';
|
||||||
|
}else{sel.style.display='none';}
|
||||||
|
});
|
||||||
|
}
|
||||||
function loadPlaylist(){
|
function loadPlaylist(){
|
||||||
const btn=document.getElementById('pl-btn');ld(btn,true);
|
const btn=document.getElementById('pl-btn');ld(btn,true);
|
||||||
fetch('/api/playlist').then(r=>r.json()).then(d=>{
|
const selEl=document.getElementById('pl-sel');
|
||||||
|
const name=selEl.style.display!=='none'&&selEl.value?'?name='+encodeURIComponent(selEl.value):'';
|
||||||
|
fetch('/api/playlist'+name).then(r=>r.json()).then(d=>{
|
||||||
ld(btn,false);
|
ld(btn,false);
|
||||||
if(d.error){document.getElementById('pl-body').innerHTML=`<tr><td colspan="4"><div class="empty">${esc(d.error)}</div></td></tr>`;return;}
|
if(d.error){document.getElementById('pl-body').innerHTML=`<tr><td colspan="4"><div class="empty">${esc(d.error)}</div></td></tr>`;return;}
|
||||||
plData=d.tracks||[];
|
plData=d.tracks||[];
|
||||||
@@ -936,33 +967,67 @@ function removeTrack(rk,btn){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Settings ──────────────────────────────────────────────────
|
// ── Settings ──────────────────────────────────────────────────
|
||||||
|
const STATION_OPTS=[
|
||||||
|
{key:'journey_fm',label:'Journey FM'},
|
||||||
|
{key:'spirit_fm',label:'Spirit FM'},
|
||||||
|
{key:'klove',label:'K-LOVE'},
|
||||||
|
];
|
||||||
|
function addPlaylistRow(pl){
|
||||||
|
const name=pl.name||'',sts=pl.stations||[];
|
||||||
|
const wrap=document.createElement('div');
|
||||||
|
wrap.style.cssText='background:var(--sf2);border:1px solid var(--bd);border-radius:10px;padding:.7rem .9rem;display:flex;flex-direction:column;gap:.5rem';
|
||||||
|
const top=document.createElement('div');
|
||||||
|
top.style.cssText='display:flex;gap:.5rem;align-items:center';
|
||||||
|
const inp=document.createElement('input');
|
||||||
|
inp.className='fi pl-name';inp.type='text';inp.placeholder='Playlist name…';inp.value=name;inp.style.flex='1';
|
||||||
|
const del=document.createElement('button');
|
||||||
|
del.className='btn btd';del.style.cssText='font-size:.74rem;padding:.3rem .55rem;flex-shrink:0';
|
||||||
|
del.textContent='✕';del.onclick=()=>wrap.remove();
|
||||||
|
top.appendChild(inp);top.appendChild(del);
|
||||||
|
const cks=document.createElement('div');
|
||||||
|
cks.style.cssText='display:flex;gap:.5rem;flex-wrap:wrap';
|
||||||
|
STATION_OPTS.forEach(opt=>{
|
||||||
|
const lbl=document.createElement('label');
|
||||||
|
lbl.style.cssText='display:inline-flex;align-items:center;gap:.35rem;font-size:.8rem;cursor:pointer;background:var(--sf);border:1px solid var(--bd);border-radius:6px;padding:.25rem .55rem;transition:border-color .15s';
|
||||||
|
lbl.onmouseenter=()=>lbl.style.borderColor='var(--am)';
|
||||||
|
lbl.onmouseleave=()=>lbl.style.borderColor='';
|
||||||
|
const cb=document.createElement('input');
|
||||||
|
cb.type='checkbox';cb.className='pl-st';cb.dataset.key=opt.key;
|
||||||
|
cb.style.accentColor='var(--am)';cb.checked=sts.includes(opt.key);
|
||||||
|
lbl.appendChild(cb);lbl.appendChild(document.createTextNode(opt.label));
|
||||||
|
cks.appendChild(lbl);
|
||||||
|
});
|
||||||
|
wrap.appendChild(top);wrap.appendChild(cks);
|
||||||
|
document.getElementById('pl-list').appendChild(wrap);
|
||||||
|
}
|
||||||
|
function getPlaylists(){
|
||||||
|
return Array.from(document.querySelectorAll('#pl-list>div')).map(wrap=>{
|
||||||
|
const name=wrap.querySelector('.pl-name')?.value.trim()||'';
|
||||||
|
const stations=Array.from(wrap.querySelectorAll('.pl-st:checked')).map(c=>c.dataset.key);
|
||||||
|
return name?{name,stations}:null;
|
||||||
|
}).filter(Boolean);
|
||||||
|
}
|
||||||
function loadCfg(){
|
function loadCfg(){
|
||||||
fetch('/api/config').then(r=>r.json()).then(cfg=>{
|
fetch('/api/config').then(r=>r.json()).then(cfg=>{
|
||||||
document.getElementById('f-sv').value=cfg.SERVER_IP||'';
|
document.getElementById('f-sv').value=cfg.SERVER_IP||'';
|
||||||
document.getElementById('f-pl').value=cfg.PLAYLIST_NAME||'';
|
|
||||||
document.getElementById('f-au').checked=!!cfg.AUTO_UPDATE;
|
document.getElementById('f-au').checked=!!cfg.AUTO_UPDATE;
|
||||||
document.getElementById('f-in').value=cfg.UPDATE_INTERVAL||15;
|
document.getElementById('f-in').value=cfg.UPDATE_INTERVAL||15;
|
||||||
document.getElementById('f-un').value=cfg.UPDATE_UNIT||'Minutes';
|
document.getElementById('f-un').value=cfg.UPDATE_UNIT||'Minutes';
|
||||||
const sts=cfg.SELECTED_STATIONS||[];
|
|
||||||
document.getElementById('st-j').checked=sts.includes('journey_fm');
|
|
||||||
document.getElementById('st-s').checked=sts.includes('spirit_fm');
|
|
||||||
document.getElementById('st-k').checked=sts.includes('klove');
|
|
||||||
const h=document.getElementById('tk-hint');
|
const h=document.getElementById('tk-hint');
|
||||||
h.textContent=cfg._has_token?'✓ Token saved — leave blank to keep it':'No token saved yet';
|
h.textContent=cfg._has_token?'✓ Token saved — leave blank to keep it':'No token saved yet';
|
||||||
h.style.color=cfg._has_token?'var(--gr)':'var(--mt)';
|
h.style.color=cfg._has_token?'var(--gr)':'var(--mt)';
|
||||||
|
const list=document.getElementById('pl-list');list.innerHTML='';
|
||||||
|
const pls=cfg.PLAYLISTS||[];
|
||||||
|
if(pls.length){pls.forEach(p=>addPlaylistRow(p));}
|
||||||
|
else{addPlaylistRow({name:'',stations:['journey_fm']});}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function doSave(){
|
function doSave(){
|
||||||
const btn=document.getElementById('save-btn');ld(btn,true);
|
const btn=document.getElementById('save-btn');ld(btn,true);
|
||||||
const stations=[];
|
|
||||||
if(document.getElementById('st-j').checked)stations.push('journey_fm');
|
|
||||||
if(document.getElementById('st-s').checked)stations.push('spirit_fm');
|
|
||||||
if(document.getElementById('st-k').checked)stations.push('klove');
|
|
||||||
fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
|
fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
|
||||||
SERVER_IP:document.getElementById('f-sv').value.trim(),
|
SERVER_IP:document.getElementById('f-sv').value.trim(),
|
||||||
PLEX_TOKEN:document.getElementById('f-tk').value.trim(),
|
PLEX_TOKEN:document.getElementById('f-tk').value.trim(),
|
||||||
PLAYLIST_NAME:document.getElementById('f-pl').value.trim(),
|
PLAYLISTS:getPlaylists(),
|
||||||
SELECTED_STATIONS:stations,
|
|
||||||
AUTO_UPDATE:document.getElementById('f-au').checked,
|
AUTO_UPDATE:document.getElementById('f-au').checked,
|
||||||
UPDATE_INTERVAL:parseInt(document.getElementById('f-in').value)||15,
|
UPDATE_INTERVAL:parseInt(document.getElementById('f-in').value)||15,
|
||||||
UPDATE_UNIT:document.getElementById('f-un').value,
|
UPDATE_UNIT:document.getElementById('f-un').value,
|
||||||
@@ -1095,7 +1160,10 @@ def _build_handler(stats_supplier):
|
|||||||
self._json(load_history_entries())
|
self._json(load_history_entries())
|
||||||
elif p == "/api/playlist":
|
elif p == "/api/playlist":
|
||||||
try:
|
try:
|
||||||
self._json(get_playlist_tracks())
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
qs = parse_qs(urlparse(self.path).query)
|
||||||
|
pname = qs.get("name", [None])[0]
|
||||||
|
self._json(get_playlist_tracks(name=pname))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._json({"error": str(exc), "tracks": []})
|
self._json({"error": str(exc), "tracks": []})
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in new issue
Block a user