Skip to content
This repository was archived by the owner on Jan 16, 2026. It is now read-only.

Commit c71a6c4

Browse files
committed
Add script to generate share hashes for audio archives from FileBrowser
1 parent e9a5dc0 commit c71a6c4

3 files changed

Lines changed: 158 additions & 16 deletions

File tree

add_share_hashes.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import asyncio
2+
import os
3+
from urllib.parse import unquote, urlparse
4+
5+
import aiohttp
6+
import asyncpg
7+
from dotenv import load_dotenv
8+
9+
load_dotenv()
10+
11+
# Database settings
12+
db_settings = {
13+
"host": os.getenv("POSTGRES_HOST"),
14+
"port": int(os.getenv("POSTGRES_PORT", 5434)),
15+
"database": os.getenv("POSTGRES_DB"),
16+
"user": os.getenv("POSTGRES_USER"),
17+
"password": os.getenv("POSTGRES_PASSWORD"),
18+
}
19+
20+
FILEBROWSER_URL = os.getenv("FILEBROWSER_URL")
21+
FILEBROWSER_USERNAME = os.getenv("FILEBROWSER_USERNAME")
22+
FILEBROWSER_PASSWORD = os.getenv("FILEBROWSER_PASSWORD")
23+
FILEBROWSER_UPLOAD_PATH = os.getenv("FILEBROWSER_UPLOAD_PATH", "HBNI-Audio/Recordings")
24+
25+
# Global dict of filebrowser items {name: full_path}
26+
FILEBROWSER_ITEMS = {}
27+
28+
29+
async def get_filebrowser_token():
30+
async with aiohttp.ClientSession() as session:
31+
async with session.post(
32+
f"{FILEBROWSER_URL}/api/login",
33+
json={"username": FILEBROWSER_USERNAME, "password": FILEBROWSER_PASSWORD},
34+
) as response:
35+
token = await response.text()
36+
return token.strip()
37+
38+
39+
async def get_public_share_url(file_relative_path: str, token: str) -> str:
40+
headers = {"X-Auth": token.strip(), "accept": "*/*"}
41+
async with aiohttp.ClientSession() as session:
42+
async with session.post(
43+
f"{FILEBROWSER_URL}/api/share/{file_relative_path}",
44+
headers=headers,
45+
json={"path": f"/{file_relative_path}"},
46+
) as response:
47+
if response.status != 200:
48+
body = await response.text()
49+
raise Exception(
50+
f"Failed to create share link: {response.status} - {body}"
51+
)
52+
data = await response.json()
53+
return data["hash"]
54+
55+
56+
async def list_filebrowser_items():
57+
global FILEBROWSER_ITEMS
58+
token = await get_filebrowser_token()
59+
headers = {"X-Auth": token, "Accept": "application/json"}
60+
61+
async with aiohttp.ClientSession() as session:
62+
async with session.get(
63+
f"{FILEBROWSER_URL}/api/resources/{FILEBROWSER_UPLOAD_PATH}",
64+
headers=headers,
65+
) as response:
66+
if response.status != 200:
67+
body = await response.text()
68+
raise Exception(f"Failed to list items: {response.status} - {body}")
69+
data = await response.json()
70+
71+
items = data.get("items", [])
72+
FILEBROWSER_ITEMS = {
73+
item["name"]: item["path"].lstrip("/") for item in items if not item["isDir"]
74+
}
75+
76+
print(f"📂 Loaded {len(FILEBROWSER_ITEMS)} files from FileBrowser.")
77+
78+
79+
def extract_filename_from_download_link(download_link: str) -> str:
80+
path = urlparse(download_link).path
81+
return unquote(os.path.basename(path)).replace("&", "&").replace("&Amp;", "&")
82+
83+
84+
async def list_audioarchives():
85+
await list_filebrowser_items()
86+
token = await get_filebrowser_token()
87+
pool = await asyncpg.create_pool(**db_settings)
88+
89+
async with pool.acquire() as conn:
90+
query = """
91+
SELECT * FROM audioarchives
92+
WHERE download_link LIKE '%play_recording%'
93+
ORDER BY date DESC;
94+
"""
95+
rows = await conn.fetch(query)
96+
print(f"🎵 Found {len(rows)} rows missing share_hash:")
97+
98+
for row in rows:
99+
download_link = row["download_link"]
100+
filename = extract_filename_from_download_link(download_link)
101+
current_hash = row["share_hash"]
102+
103+
# if current_hash:
104+
# print(f"✅ Already has share_hash: {filename} → {current_hash}")
105+
# continue
106+
107+
filebrowser_path = FILEBROWSER_ITEMS.get(filename)
108+
if not filebrowser_path:
109+
print(f"⚠️ File not found in FileBrowser: {filename}")
110+
continue
111+
112+
try:
113+
share_hash = await get_public_share_url(filebrowser_path, token)
114+
update_query = (
115+
"UPDATE audioarchives SET share_hash = $1 WHERE download_link = $2;"
116+
)
117+
await conn.execute(update_query, share_hash, download_link)
118+
print(f"✅ Added share_hash for: {filename}{share_hash}")
119+
except Exception as e:
120+
print(f"❌ Failed to create share for {filename}: {str(e)}")
121+
122+
await pool.close()
123+
124+
125+
if __name__ == "__main__":
126+
asyncio.run(list_audioarchives())

main.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,17 @@
99
import threading
1010
import time
1111
from datetime import datetime, timedelta
12-
from logging.handlers import RotatingFileHandler
13-
from typing import Callable, Literal
12+
from typing import Callable
1413

1514
import asyncpg
1615
import requests
1716
from dotenv import load_dotenv
17+
from natsort import natsorted
1818

1919
import filebrowser_uploader
2020
import firebase_android_notification
2121
import firebase_web_notification
2222
import send_email
23-
import synology_uploader
2423
import zip_file
2524

2625
load_dotenv()
@@ -295,7 +294,7 @@ def remove_stream(self, host: str):
295294
self.loop.create_task(self.update_recording_status())
296295

297296
def run(self):
298-
self.send_notification()
297+
# self.send_notification()
299298

300299
# stream = Stream("Springhill", "http://hbniaudio.hbni.net:443", "springhill", "Springhill/Odanah/Cascade singing in memory of Dave Stahl(Bon Homme)", self.remove_stream)
301300
# self.active_streams["springhill"] = stream
@@ -398,21 +397,38 @@ def send_notification(self):
398397

399398
class CustomHandler(http.server.SimpleHTTPRequestHandler):
400399
def __init__(self, *args, **kwargs):
401-
super().__init__(*args, directory="logs", **kwargs)
400+
# Serve from root so we can access logs/ directory
401+
super().__init__(*args, directory=".", **kwargs)
402402
load_dotenv()
403403

404404
def do_GET(self):
405-
if self.path.endswith(".log"): # Check if the requested file is a log file
406-
log_file_path = os.path.join("logs", self.path.lstrip("/"))
407-
if os.path.exists(log_file_path): # Ensure the file exists
408-
self.send_response(200)
409-
self.send_header("Content-type", "text/plain")
410-
self.end_headers()
411-
with open(log_file_path, "r", encoding="utf-8") as log_file:
412-
self.wfile.write(log_file.read().encode("utf-8"))
413-
return
414-
# Default behavior for other files
415-
super().do_GET()
405+
# Show a custom index with log links
406+
if self.path == "/":
407+
self.send_response(200)
408+
self.send_header("Content-type", "text/html")
409+
self.end_headers()
410+
411+
html = "<html><body><h1>Log Files</h1><ul>"
412+
for filename in natsorted(os.listdir("logs")):
413+
414+
if filename.endswith(".log"):
415+
html += f'<li><a href="/logs/{filename}">{filename}</a></li>'
416+
html += "</ul></body></html>"
417+
418+
self.wfile.write(html.encode("utf-8"))
419+
return
420+
421+
# Serve logs with text/plain so they show in browser
422+
if self.path.startswith("/logs/") and self.path.endswith(".log"):
423+
self.send_response(200)
424+
self.send_header("Content-type", "text/plain")
425+
self.end_headers()
426+
filepath = os.path.join("logs", os.path.basename(self.path))
427+
with open(filepath, "r", encoding="utf-8") as f:
428+
self.wfile.write(f.read().encode("utf-8"))
429+
return
430+
431+
return super().do_GET()
416432

417433

418434
def start_log_server():

requirements.txt

260 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)