Spaces:
Running
Running
Vaibhav Pharmacy commited on
Commit ·
96bdeb2
1
Parent(s): 5db7064
Harden all edge cases: auto-reconnect with health check, thread-safe connection pool, scoped artist CTEs (137K→30 row scan), ttl_cache on stats/filters, invalidate_cache on mutations, safe is_watched cast, remove test files
Browse files- .gitignore +2 -0
- api/main.py +39 -20
- collector/db.py +38 -10
- test_db.py +0 -10
- test_perf.py +0 -53
- test_perf_artist.py +0 -65
.gitignore
CHANGED
|
@@ -48,3 +48,5 @@ frontend/.swc/
|
|
| 48 |
frontend/public/logo.png
|
| 49 |
frontend/src/app/favicon.ico
|
| 50 |
db/.fuse_hidden*
|
|
|
|
|
|
|
|
|
| 48 |
frontend/public/logo.png
|
| 49 |
frontend/src/app/favicon.ico
|
| 50 |
db/.fuse_hidden*
|
| 51 |
+
*.pyc
|
| 52 |
+
test_*.py
|
api/main.py
CHANGED
|
@@ -18,6 +18,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 18 |
from pydantic import BaseModel
|
| 19 |
from db import get_connection, init_db
|
| 20 |
from metrics import compute_artist_metrics, enrich_song_with_metrics, enrich_viral_alert, enrich_songs_bulk, enrich_viral_alerts_bulk
|
|
|
|
| 21 |
|
| 22 |
app = FastAPI(title="Music Intelligence Dashboard API", version="1.0.0")
|
| 23 |
|
|
@@ -1029,6 +1030,10 @@ def get_spotify_artists(
|
|
| 1029 |
@app.get("/api/filters")
|
| 1030 |
def get_filter_options():
|
| 1031 |
"""Get available genre and region options for filter dropdowns."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1032 |
conn = get_connection()
|
| 1033 |
genres = [r[0] for r in conn.execute(
|
| 1034 |
"SELECT DISTINCT genre FROM artists WHERE genre IS NOT NULL ORDER BY genre"
|
|
@@ -1052,17 +1057,22 @@ def get_artist_detail(artist_id: str, platform: str = Query("youtube")):
|
|
| 1052 |
conn.close()
|
| 1053 |
raise HTTPException(status_code=404, detail="Artist not found")
|
| 1054 |
|
| 1055 |
-
# Get songs for this platform
|
| 1056 |
songs = conn.execute("""
|
| 1057 |
-
WITH
|
| 1058 |
-
SELECT
|
| 1059 |
-
|
| 1060 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1061 |
),
|
| 1062 |
first_snaps AS (
|
| 1063 |
-
SELECT song_id, MIN(id) as min_id
|
| 1064 |
-
FROM play_snapshots
|
| 1065 |
-
|
|
|
|
| 1066 |
)
|
| 1067 |
SELECT
|
| 1068 |
s.*,
|
|
@@ -1081,7 +1091,7 @@ def get_artist_detail(artist_id: str, platform: str = Query("youtube")):
|
|
| 1081 |
LEFT JOIN play_snapshots fs_data ON fs.min_id = fs_data.id
|
| 1082 |
WHERE s.artist_id = ? AND s.platform = ?
|
| 1083 |
ORDER BY latest_play_count DESC NULLS LAST
|
| 1084 |
-
""", (artist_id, platform)).fetchall()
|
| 1085 |
|
| 1086 |
# Get viral alerts for this artist
|
| 1087 |
alerts = conn.execute("""
|
|
@@ -1123,17 +1133,20 @@ def get_spotify_artist_detail(artist_id: str):
|
|
| 1123 |
raise HTTPException(status_code=404, detail="Spotify artist not found")
|
| 1124 |
|
| 1125 |
songs = conn.execute("""
|
| 1126 |
-
WITH
|
| 1127 |
-
SELECT
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
|
|
|
|
|
|
|
|
|
| 1131 |
),
|
| 1132 |
first_snaps AS (
|
| 1133 |
-
SELECT song_id, MIN(id) as min_id
|
| 1134 |
-
FROM play_snapshots
|
| 1135 |
-
WHERE platform = 'spotify'
|
| 1136 |
-
GROUP BY song_id
|
| 1137 |
)
|
| 1138 |
SELECT
|
| 1139 |
s.*,
|
|
@@ -1147,7 +1160,7 @@ def get_spotify_artist_detail(artist_id: str):
|
|
| 1147 |
LEFT JOIN play_snapshots fs_data ON fs.min_id = fs_data.id
|
| 1148 |
WHERE s.artist_id = ? AND s.platform = 'spotify'
|
| 1149 |
ORDER BY latest_play_count DESC NULLS LAST
|
| 1150 |
-
""", (artist_id,)).fetchall()
|
| 1151 |
|
| 1152 |
alerts = conn.execute("""
|
| 1153 |
SELECT
|
|
@@ -1197,10 +1210,11 @@ def toggle_watchlist(artist_id: str):
|
|
| 1197 |
conn.close()
|
| 1198 |
raise HTTPException(status_code=404, detail="Artist not found")
|
| 1199 |
|
| 1200 |
-
new_status = 0 if artist["is_watched"] else 1
|
| 1201 |
conn.execute("UPDATE artists SET is_watched = ? WHERE id = ?", (new_status, artist_id))
|
| 1202 |
conn.commit()
|
| 1203 |
conn.close()
|
|
|
|
| 1204 |
return {"artist_id": artist_id, "is_watched": bool(new_status)}
|
| 1205 |
|
| 1206 |
|
|
@@ -1236,6 +1250,7 @@ def delete_artist(artist_id: str):
|
|
| 1236 |
conn.execute("DELETE FROM artists WHERE id = ?", (artist_id,))
|
| 1237 |
conn.commit()
|
| 1238 |
conn.close()
|
|
|
|
| 1239 |
|
| 1240 |
return {
|
| 1241 |
"status": "deleted",
|
|
@@ -1249,6 +1264,10 @@ def delete_artist(artist_id: str):
|
|
| 1249 |
@app.get("/api/stats")
|
| 1250 |
def get_stats():
|
| 1251 |
"""Get overall dashboard statistics."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1252 |
conn = get_connection()
|
| 1253 |
counts = conn.execute("""
|
| 1254 |
SELECT
|
|
|
|
| 18 |
from pydantic import BaseModel
|
| 19 |
from db import get_connection, init_db
|
| 20 |
from metrics import compute_artist_metrics, enrich_song_with_metrics, enrich_viral_alert, enrich_songs_bulk, enrich_viral_alerts_bulk
|
| 21 |
+
from cache import ttl_cache, invalidate_cache
|
| 22 |
|
| 23 |
app = FastAPI(title="Music Intelligence Dashboard API", version="1.0.0")
|
| 24 |
|
|
|
|
| 1030 |
@app.get("/api/filters")
|
| 1031 |
def get_filter_options():
|
| 1032 |
"""Get available genre and region options for filter dropdowns."""
|
| 1033 |
+
return _get_filters_cached()
|
| 1034 |
+
|
| 1035 |
+
@ttl_cache(ttl_seconds=300)
|
| 1036 |
+
def _get_filters_cached():
|
| 1037 |
conn = get_connection()
|
| 1038 |
genres = [r[0] for r in conn.execute(
|
| 1039 |
"SELECT DISTINCT genre FROM artists WHERE genre IS NOT NULL ORDER BY genre"
|
|
|
|
| 1057 |
conn.close()
|
| 1058 |
raise HTTPException(status_code=404, detail="Artist not found")
|
| 1059 |
|
| 1060 |
+
# Get songs for this platform — CTEs scoped to this artist's songs only
|
| 1061 |
songs = conn.execute("""
|
| 1062 |
+
WITH artist_song_ids AS (
|
| 1063 |
+
SELECT id FROM songs WHERE artist_id = ? AND platform = ?
|
| 1064 |
+
),
|
| 1065 |
+
latest_snaps AS (
|
| 1066 |
+
SELECT ps.song_id, MAX(ps.id) as max_id
|
| 1067 |
+
FROM play_snapshots ps
|
| 1068 |
+
WHERE ps.song_id IN (SELECT id FROM artist_song_ids)
|
| 1069 |
+
GROUP BY ps.song_id
|
| 1070 |
),
|
| 1071 |
first_snaps AS (
|
| 1072 |
+
SELECT ps.song_id, MIN(ps.id) as min_id
|
| 1073 |
+
FROM play_snapshots ps
|
| 1074 |
+
WHERE ps.song_id IN (SELECT id FROM artist_song_ids)
|
| 1075 |
+
GROUP BY ps.song_id
|
| 1076 |
)
|
| 1077 |
SELECT
|
| 1078 |
s.*,
|
|
|
|
| 1091 |
LEFT JOIN play_snapshots fs_data ON fs.min_id = fs_data.id
|
| 1092 |
WHERE s.artist_id = ? AND s.platform = ?
|
| 1093 |
ORDER BY latest_play_count DESC NULLS LAST
|
| 1094 |
+
""", (artist_id, platform, artist_id, platform)).fetchall()
|
| 1095 |
|
| 1096 |
# Get viral alerts for this artist
|
| 1097 |
alerts = conn.execute("""
|
|
|
|
| 1133 |
raise HTTPException(status_code=404, detail="Spotify artist not found")
|
| 1134 |
|
| 1135 |
songs = conn.execute("""
|
| 1136 |
+
WITH artist_song_ids AS (
|
| 1137 |
+
SELECT id FROM songs WHERE artist_id = ? AND platform = 'spotify'
|
| 1138 |
+
),
|
| 1139 |
+
latest_snaps AS (
|
| 1140 |
+
SELECT ps.song_id, MAX(ps.id) as max_id
|
| 1141 |
+
FROM play_snapshots ps
|
| 1142 |
+
WHERE ps.song_id IN (SELECT id FROM artist_song_ids) AND ps.platform = 'spotify'
|
| 1143 |
+
GROUP BY ps.song_id
|
| 1144 |
),
|
| 1145 |
first_snaps AS (
|
| 1146 |
+
SELECT ps.song_id, MIN(ps.id) as min_id
|
| 1147 |
+
FROM play_snapshots ps
|
| 1148 |
+
WHERE ps.song_id IN (SELECT id FROM artist_song_ids) AND ps.platform = 'spotify'
|
| 1149 |
+
GROUP BY ps.song_id
|
| 1150 |
)
|
| 1151 |
SELECT
|
| 1152 |
s.*,
|
|
|
|
| 1160 |
LEFT JOIN play_snapshots fs_data ON fs.min_id = fs_data.id
|
| 1161 |
WHERE s.artist_id = ? AND s.platform = 'spotify'
|
| 1162 |
ORDER BY latest_play_count DESC NULLS LAST
|
| 1163 |
+
""", (artist_id, artist_id)).fetchall()
|
| 1164 |
|
| 1165 |
alerts = conn.execute("""
|
| 1166 |
SELECT
|
|
|
|
| 1210 |
conn.close()
|
| 1211 |
raise HTTPException(status_code=404, detail="Artist not found")
|
| 1212 |
|
| 1213 |
+
new_status = 0 if int(artist["is_watched"] or 0) else 1
|
| 1214 |
conn.execute("UPDATE artists SET is_watched = ? WHERE id = ?", (new_status, artist_id))
|
| 1215 |
conn.commit()
|
| 1216 |
conn.close()
|
| 1217 |
+
invalidate_cache()
|
| 1218 |
return {"artist_id": artist_id, "is_watched": bool(new_status)}
|
| 1219 |
|
| 1220 |
|
|
|
|
| 1250 |
conn.execute("DELETE FROM artists WHERE id = ?", (artist_id,))
|
| 1251 |
conn.commit()
|
| 1252 |
conn.close()
|
| 1253 |
+
invalidate_cache()
|
| 1254 |
|
| 1255 |
return {
|
| 1256 |
"status": "deleted",
|
|
|
|
| 1264 |
@app.get("/api/stats")
|
| 1265 |
def get_stats():
|
| 1266 |
"""Get overall dashboard statistics."""
|
| 1267 |
+
return _get_stats_cached()
|
| 1268 |
+
|
| 1269 |
+
@ttl_cache(ttl_seconds=60)
|
| 1270 |
+
def _get_stats_cached():
|
| 1271 |
conn = get_connection()
|
| 1272 |
counts = conn.execute("""
|
| 1273 |
SELECT
|
collector/db.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
| 2 |
|
| 3 |
import sqlite3
|
| 4 |
import os
|
|
|
|
|
|
|
| 5 |
from datetime import datetime, timezone, timedelta
|
| 6 |
from pathlib import Path
|
| 7 |
from dotenv import load_dotenv
|
|
@@ -64,29 +66,50 @@ class LibsqlDictConnection:
|
|
| 64 |
def executescript(self, sql):
|
| 65 |
self.conn.executescript(sql)
|
| 66 |
|
|
|
|
|
|
|
|
|
|
| 67 |
def commit(self):
|
| 68 |
self.conn.commit()
|
| 69 |
|
| 70 |
def close(self):
|
| 71 |
-
#
|
| 72 |
pass
|
| 73 |
|
| 74 |
_TURSO_GLOBAL_CONN = None
|
|
|
|
|
|
|
| 75 |
|
| 76 |
def get_connection():
|
| 77 |
-
"""Get a SQLite connection with WAL mode for better concurrency.
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
if _USE_TURSO:
|
| 80 |
if libsql is None:
|
| 81 |
raise ImportError(
|
| 82 |
"libsql_experimental is required for Turso connections. "
|
| 83 |
"Install it with: pip install libsql-experimental"
|
| 84 |
)
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
_TURSO_GLOBAL_CONN
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
else:
|
| 91 |
# Local SQLite fallback for development
|
| 92 |
conn = sqlite3.connect(str(DB_PATH))
|
|
@@ -124,9 +147,13 @@ def init_db():
|
|
| 124 |
else:
|
| 125 |
print(f"Connected to Turso at {_TURSO_URL}")
|
| 126 |
|
|
|
|
| 127 |
|
| 128 |
def ensure_indexes():
|
| 129 |
-
"""Create performance indexes if they don't exist."""
|
|
|
|
|
|
|
|
|
|
| 130 |
conn = get_connection()
|
| 131 |
indexes = [
|
| 132 |
"CREATE INDEX IF NOT EXISTS idx_snapshots_song_platform_date ON play_snapshots(song_id, platform, collected_at DESC)",
|
|
@@ -142,9 +169,10 @@ def ensure_indexes():
|
|
| 142 |
conn.commit()
|
| 143 |
except Exception as e:
|
| 144 |
print(f"[indexes] Skipping: {e}")
|
|
|
|
| 145 |
print("[indexes] Performance indexes ensured.")
|
| 146 |
|
| 147 |
-
# Auto-create indexes on import
|
| 148 |
try:
|
| 149 |
ensure_indexes()
|
| 150 |
except Exception:
|
|
|
|
| 2 |
|
| 3 |
import sqlite3
|
| 4 |
import os
|
| 5 |
+
import time
|
| 6 |
+
import threading
|
| 7 |
from datetime import datetime, timezone, timedelta
|
| 8 |
from pathlib import Path
|
| 9 |
from dotenv import load_dotenv
|
|
|
|
| 66 |
def executescript(self, sql):
|
| 67 |
self.conn.executescript(sql)
|
| 68 |
|
| 69 |
+
def executemany(self, sql, params_list):
|
| 70 |
+
self.conn.executemany(sql, params_list)
|
| 71 |
+
|
| 72 |
def commit(self):
|
| 73 |
self.conn.commit()
|
| 74 |
|
| 75 |
def close(self):
|
| 76 |
+
# No-op for pooled Turso connection — connection is reused globally
|
| 77 |
pass
|
| 78 |
|
| 79 |
_TURSO_GLOBAL_CONN = None
|
| 80 |
+
_TURSO_LAST_OK = 0 # timestamp of last successful use
|
| 81 |
+
_TURSO_LOCK = threading.Lock()
|
| 82 |
|
| 83 |
def get_connection():
|
| 84 |
+
"""Get a SQLite connection with WAL mode for better concurrency.
|
| 85 |
+
|
| 86 |
+
For Turso: uses a global pooled connection with auto-reconnect.
|
| 87 |
+
Health-checks the connection if idle for >30 seconds.
|
| 88 |
+
"""
|
| 89 |
+
global _TURSO_GLOBAL_CONN, _TURSO_LAST_OK
|
| 90 |
if _USE_TURSO:
|
| 91 |
if libsql is None:
|
| 92 |
raise ImportError(
|
| 93 |
"libsql_experimental is required for Turso connections. "
|
| 94 |
"Install it with: pip install libsql-experimental"
|
| 95 |
)
|
| 96 |
+
with _TURSO_LOCK:
|
| 97 |
+
now = time.time()
|
| 98 |
+
# Health check only if connection has been idle for >30 seconds
|
| 99 |
+
if _TURSO_GLOBAL_CONN is not None and (now - _TURSO_LAST_OK) > 30:
|
| 100 |
+
try:
|
| 101 |
+
_TURSO_GLOBAL_CONN.execute("SELECT 1").fetchone()
|
| 102 |
+
except Exception:
|
| 103 |
+
print("[db] Turso connection stale, reconnecting...")
|
| 104 |
+
_TURSO_GLOBAL_CONN = None
|
| 105 |
+
# Create new connection if needed
|
| 106 |
+
if _TURSO_GLOBAL_CONN is None:
|
| 107 |
+
safe_url = _TURSO_URL.replace("libsql://", "https://").replace("wss://", "https://")
|
| 108 |
+
conn = libsql.connect(safe_url, auth_token=_TURSO_TOKEN)
|
| 109 |
+
_TURSO_GLOBAL_CONN = LibsqlDictConnection(conn)
|
| 110 |
+
print("[db] Turso connection established.")
|
| 111 |
+
_TURSO_LAST_OK = now
|
| 112 |
+
return _TURSO_GLOBAL_CONN
|
| 113 |
else:
|
| 114 |
# Local SQLite fallback for development
|
| 115 |
conn = sqlite3.connect(str(DB_PATH))
|
|
|
|
| 147 |
else:
|
| 148 |
print(f"Connected to Turso at {_TURSO_URL}")
|
| 149 |
|
| 150 |
+
_INDEXES_CREATED = False
|
| 151 |
|
| 152 |
def ensure_indexes():
|
| 153 |
+
"""Create performance indexes if they don't exist. Runs once per process."""
|
| 154 |
+
global _INDEXES_CREATED
|
| 155 |
+
if _INDEXES_CREATED:
|
| 156 |
+
return
|
| 157 |
conn = get_connection()
|
| 158 |
indexes = [
|
| 159 |
"CREATE INDEX IF NOT EXISTS idx_snapshots_song_platform_date ON play_snapshots(song_id, platform, collected_at DESC)",
|
|
|
|
| 169 |
conn.commit()
|
| 170 |
except Exception as e:
|
| 171 |
print(f"[indexes] Skipping: {e}")
|
| 172 |
+
_INDEXES_CREATED = True
|
| 173 |
print("[indexes] Performance indexes ensured.")
|
| 174 |
|
| 175 |
+
# Auto-create indexes on first import (runs once)
|
| 176 |
try:
|
| 177 |
ensure_indexes()
|
| 178 |
except Exception:
|
test_db.py
DELETED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
import os, sys
|
| 2 |
-
sys.path.insert(0, 'collector')
|
| 3 |
-
from dotenv import load_dotenv
|
| 4 |
-
import db
|
| 5 |
-
|
| 6 |
-
load_dotenv()
|
| 7 |
-
conn = db.get_connection()
|
| 8 |
-
res = conn.execute("SELECT release_date, title, platform FROM songs ORDER BY release_date DESC LIMIT 5").fetchall()
|
| 9 |
-
for r in res:
|
| 10 |
-
print(r)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_perf.py
DELETED
|
@@ -1,53 +0,0 @@
|
|
| 1 |
-
import os, sys, time
|
| 2 |
-
sys.path.insert(0, 'collector')
|
| 3 |
-
from dotenv import load_dotenv
|
| 4 |
-
import db
|
| 5 |
-
|
| 6 |
-
load_dotenv()
|
| 7 |
-
conn = db.get_connection()
|
| 8 |
-
|
| 9 |
-
q1 = """
|
| 10 |
-
SELECT
|
| 11 |
-
a.id,
|
| 12 |
-
(SELECT COUNT(*) FROM songs WHERE artist_id = a.id AND platform = 'youtube') as yt_song_count,
|
| 13 |
-
(SELECT SUM(ps.play_count) FROM play_snapshots ps
|
| 14 |
-
JOIN songs s ON ps.song_id = s.id
|
| 15 |
-
WHERE s.artist_id = a.id AND ps.platform = 'youtube'
|
| 16 |
-
AND ps.id IN (SELECT MAX(id) FROM play_snapshots GROUP BY song_id)
|
| 17 |
-
) as total_yt_views
|
| 18 |
-
FROM artists a
|
| 19 |
-
LIMIT 5
|
| 20 |
-
"""
|
| 21 |
-
|
| 22 |
-
q2 = """
|
| 23 |
-
WITH latest_snapshots AS (
|
| 24 |
-
SELECT song_id, MAX(id) as max_id
|
| 25 |
-
FROM play_snapshots
|
| 26 |
-
GROUP BY song_id
|
| 27 |
-
),
|
| 28 |
-
song_stats AS (
|
| 29 |
-
SELECT
|
| 30 |
-
s.artist_id,
|
| 31 |
-
COUNT(CASE WHEN s.platform = 'youtube' THEN 1 END) as yt_song_count,
|
| 32 |
-
SUM(CASE WHEN s.platform = 'youtube' THEN ps.play_count ELSE 0 END) as total_yt_views
|
| 33 |
-
FROM songs s
|
| 34 |
-
LEFT JOIN latest_snapshots ls ON s.id = ls.song_id
|
| 35 |
-
LEFT JOIN play_snapshots ps ON ls.max_id = ps.id
|
| 36 |
-
GROUP BY s.artist_id
|
| 37 |
-
)
|
| 38 |
-
SELECT
|
| 39 |
-
a.id,
|
| 40 |
-
COALESCE(ss.yt_song_count, 0) as yt_song_count,
|
| 41 |
-
COALESCE(ss.total_yt_views, 0) as total_yt_views
|
| 42 |
-
FROM artists a
|
| 43 |
-
LEFT JOIN song_stats ss ON a.id = ss.artist_id
|
| 44 |
-
LIMIT 5
|
| 45 |
-
"""
|
| 46 |
-
|
| 47 |
-
start = time.time()
|
| 48 |
-
conn.execute(q1).fetchall()
|
| 49 |
-
print("Q1 (Old):", time.time() - start)
|
| 50 |
-
|
| 51 |
-
start = time.time()
|
| 52 |
-
conn.execute(q2).fetchall()
|
| 53 |
-
print("Q2 (New):", time.time() - start)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_perf_artist.py
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
import os, sys, time
|
| 2 |
-
sys.path.insert(0, 'collector')
|
| 3 |
-
from dotenv import load_dotenv
|
| 4 |
-
import db
|
| 5 |
-
|
| 6 |
-
load_dotenv()
|
| 7 |
-
conn = db.get_connection()
|
| 8 |
-
|
| 9 |
-
artist_id = "arooh"
|
| 10 |
-
platform = "youtube"
|
| 11 |
-
|
| 12 |
-
q1 = """
|
| 13 |
-
SELECT
|
| 14 |
-
s.*,
|
| 15 |
-
(SELECT play_count FROM play_snapshots
|
| 16 |
-
WHERE song_id = s.id ORDER BY collected_at DESC LIMIT 1) as latest_play_count,
|
| 17 |
-
(SELECT play_count FROM play_snapshots
|
| 18 |
-
WHERE song_id = s.id ORDER BY collected_at ASC LIMIT 1) as first_play_count,
|
| 19 |
-
(SELECT like_count FROM play_snapshots
|
| 20 |
-
WHERE song_id = s.id ORDER BY collected_at DESC LIMIT 1) as latest_like_count,
|
| 21 |
-
(SELECT comment_count FROM play_snapshots
|
| 22 |
-
WHERE song_id = s.id ORDER BY collected_at DESC LIMIT 1) as latest_comment_count,
|
| 23 |
-
(SELECT ytmusic_play_count FROM play_snapshots
|
| 24 |
-
WHERE song_id = s.id AND ytmusic_play_count IS NOT NULL ORDER BY collected_at DESC LIMIT 1) as ytmusic_play_count
|
| 25 |
-
FROM songs s
|
| 26 |
-
WHERE s.artist_id = ? AND s.platform = ?
|
| 27 |
-
ORDER BY latest_play_count DESC NULLS LAST
|
| 28 |
-
"""
|
| 29 |
-
|
| 30 |
-
q2 = """
|
| 31 |
-
WITH latest_snaps AS (
|
| 32 |
-
SELECT song_id, MAX(id) as max_id
|
| 33 |
-
FROM play_snapshots
|
| 34 |
-
WHERE platform = ?
|
| 35 |
-
GROUP BY song_id
|
| 36 |
-
),
|
| 37 |
-
first_snaps AS (
|
| 38 |
-
SELECT song_id, MIN(id) as min_id
|
| 39 |
-
FROM play_snapshots
|
| 40 |
-
WHERE platform = ?
|
| 41 |
-
GROUP BY song_id
|
| 42 |
-
)
|
| 43 |
-
SELECT
|
| 44 |
-
s.*,
|
| 45 |
-
ls_data.play_count as latest_play_count,
|
| 46 |
-
fs_data.play_count as first_play_count,
|
| 47 |
-
ls_data.like_count as latest_like_count,
|
| 48 |
-
ls_data.comment_count as latest_comment_count,
|
| 49 |
-
ls_data.ytmusic_play_count as ytmusic_play_count
|
| 50 |
-
FROM songs s
|
| 51 |
-
LEFT JOIN latest_snaps ls ON s.id = ls.song_id
|
| 52 |
-
LEFT JOIN play_snapshots ls_data ON ls.max_id = ls_data.id
|
| 53 |
-
LEFT JOIN first_snaps fs ON s.id = fs.song_id
|
| 54 |
-
LEFT JOIN play_snapshots fs_data ON fs.min_id = fs_data.id
|
| 55 |
-
WHERE s.artist_id = ? AND s.platform = ?
|
| 56 |
-
ORDER BY latest_play_count DESC NULLS LAST
|
| 57 |
-
"""
|
| 58 |
-
|
| 59 |
-
start = time.time()
|
| 60 |
-
conn.execute(q1, (artist_id, platform)).fetchall()
|
| 61 |
-
print("Q1 (Old):", time.time() - start)
|
| 62 |
-
|
| 63 |
-
start = time.time()
|
| 64 |
-
conn.execute(q2, (platform, platform, artist_id, platform)).fetchall()
|
| 65 |
-
print("Q2 (New):", time.time() - start)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|