Spaces:
Sleeping
Sleeping
Upload 11 files
Browse files- app.py +3 -129
- templates/index.html +0 -121
- templates/settings.html +2 -114
app.py
CHANGED
|
@@ -227,80 +227,12 @@ def logout():
|
|
| 227 |
@app.route('/settings')
|
| 228 |
@login_required
|
| 229 |
def settings():
|
| 230 |
-
|
| 231 |
-
user_id = session.get('user_id')
|
| 232 |
-
db = get_db()
|
| 233 |
-
users = db.users
|
| 234 |
-
user = users.find_one({'_id': ObjectId(user_id)})
|
| 235 |
-
has_oauth = bool(user and user.get('oauth_token'))
|
| 236 |
-
|
| 237 |
-
return render_template('settings.html', has_oauth=has_oauth)
|
| 238 |
|
| 239 |
@app.route('/')
|
| 240 |
@login_required
|
| 241 |
def index():
|
| 242 |
-
|
| 243 |
-
user_id = session.get('user_id')
|
| 244 |
-
user = get_db().users.find_one({'_id': ObjectId(user_id)})
|
| 245 |
-
has_oauth = bool(user and user.get('oauth_token'))
|
| 246 |
-
return render_template('index.html', has_oauth=has_oauth)
|
| 247 |
-
|
| 248 |
-
@app.route('/api/oauth/start', methods=['POST'])
|
| 249 |
-
@login_required
|
| 250 |
-
def api_oauth_start():
|
| 251 |
-
client_id = os.getenv('YOUTUBE_CLIENT_ID')
|
| 252 |
-
if not client_id:
|
| 253 |
-
return jsonify({"error": "YOUTUBE_CLIENT_ID not configured"}), 500
|
| 254 |
-
|
| 255 |
-
data = {
|
| 256 |
-
"client_id": client_id,
|
| 257 |
-
"scope": "https://www.googleapis.com/auth/youtube"
|
| 258 |
-
}
|
| 259 |
-
resp = requests.post("https://oauth2.googleapis.com/device/code", data=data)
|
| 260 |
-
if resp.status_code != 200:
|
| 261 |
-
return jsonify({"error": "Failed to contact Google OAuth"}), 500
|
| 262 |
-
|
| 263 |
-
return jsonify(resp.json())
|
| 264 |
-
|
| 265 |
-
@app.route('/api/oauth/poll', methods=['POST'])
|
| 266 |
-
@login_required
|
| 267 |
-
def api_oauth_poll():
|
| 268 |
-
client_id = os.getenv('YOUTUBE_CLIENT_ID')
|
| 269 |
-
client_secret = os.getenv('YOUTUBE_CLIENT_SECRET')
|
| 270 |
-
device_code = request.json.get('device_code')
|
| 271 |
-
|
| 272 |
-
if not client_id or not client_secret or not device_code:
|
| 273 |
-
return jsonify({"error": "Missing credentials"}), 400
|
| 274 |
-
|
| 275 |
-
data = {
|
| 276 |
-
"client_id": client_id,
|
| 277 |
-
"client_secret": client_secret,
|
| 278 |
-
"device_code": device_code,
|
| 279 |
-
"grant_type": "urn:ietf:params:oauth:grant-type:device_code"
|
| 280 |
-
}
|
| 281 |
-
|
| 282 |
-
resp = requests.post("https://oauth2.googleapis.com/token", data=data)
|
| 283 |
-
|
| 284 |
-
if resp.status_code == 200:
|
| 285 |
-
token_data = resp.json()
|
| 286 |
-
token_data["client_id"] = client_id
|
| 287 |
-
token_data["client_secret"] = client_secret
|
| 288 |
-
from bson.objectid import ObjectId
|
| 289 |
-
db = get_db()
|
| 290 |
-
db.users.update_one({'_id': ObjectId(session['user_id'])}, {'$set': {'oauth_token': json.dumps(token_data)}})
|
| 291 |
-
return jsonify({"status": "success", "token": token_data})
|
| 292 |
-
else:
|
| 293 |
-
try:
|
| 294 |
-
err_data = resp.json()
|
| 295 |
-
err = err_data.get("error")
|
| 296 |
-
if err in ["authorization_pending", "slow_down"]:
|
| 297 |
-
return jsonify({"status": "pending"})
|
| 298 |
-
elif err == "expired_token":
|
| 299 |
-
return jsonify({"status": "expired"}), 400
|
| 300 |
-
except Exception:
|
| 301 |
-
pass
|
| 302 |
-
|
| 303 |
-
return jsonify({"error": resp.text}), 500
|
| 304 |
|
| 305 |
@app.route('/api/status')
|
| 306 |
def api_status():
|
|
@@ -554,68 +486,10 @@ def api_voice():
|
|
| 554 |
return jsonify({"speak": "Skipping.", "action": "next_native"})
|
| 555 |
if song:
|
| 556 |
try:
|
| 557 |
-
user_id = session.get('user_id')
|
| 558 |
-
user = None
|
| 559 |
-
if user_id:
|
| 560 |
-
from bson.objectid import ObjectId
|
| 561 |
-
user = get_db().users.find_one({'_id': ObjectId(user_id)})
|
| 562 |
-
|
| 563 |
-
has_oauth = False
|
| 564 |
# Inject a Chrome-impersonated session to bypass YouTube bot blocking on Hugging Face Spaces
|
| 565 |
custom_session = cffi_requests.Session(impersonate="chrome")
|
| 566 |
-
|
| 567 |
-
if user and user.get('oauth_token'):
|
| 568 |
-
filepath = f"oauth_{user_id}.json"
|
| 569 |
-
with open(filepath, 'w') as f:
|
| 570 |
-
f.write(user['oauth_token'])
|
| 571 |
-
ytmusic = YTMusic(filepath, requests_session=custom_session)
|
| 572 |
-
has_oauth = True
|
| 573 |
-
# Read back in case of refresh
|
| 574 |
-
with open(filepath, 'r') as f:
|
| 575 |
-
refreshed = f.read()
|
| 576 |
-
if refreshed != user['oauth_token']:
|
| 577 |
-
get_db().users.update_one({'_id': ObjectId(user_id)}, {'$set': {'oauth_token': refreshed}})
|
| 578 |
-
else:
|
| 579 |
-
ytmusic = YTMusic(requests_session=custom_session)
|
| 580 |
-
except Exception:
|
| 581 |
-
ytmusic = YTMusic(requests_session=custom_session)
|
| 582 |
-
|
| 583 |
-
is_personal = any(phrase in song.lower() for phrase in ["my liked", "my playlist", "my mix", "my supermix"])
|
| 584 |
|
| 585 |
-
if is_personal:
|
| 586 |
-
if not has_oauth:
|
| 587 |
-
return jsonify({"speak": "Please link your Google Account in the settings first to access your personal library."})
|
| 588 |
-
|
| 589 |
-
if "liked" in song.lower():
|
| 590 |
-
playlist = ytmusic.get_liked_songs(limit=50)
|
| 591 |
-
video_ids = [track['videoId'] for track in playlist['tracks'] if track.get('videoId')]
|
| 592 |
-
return jsonify({"speak": "Playing your liked songs.", "action": "play_native", "video_ids": video_ids, "title": "Liked Songs"})
|
| 593 |
-
|
| 594 |
-
elif "playlist" in song.lower() or "mix" in song.lower():
|
| 595 |
-
# Extract requested playlist name
|
| 596 |
-
pl_name = song.lower().replace("my playlist", "").replace("my mix", "").replace("my supermix", "my supermix").strip()
|
| 597 |
-
if not pl_name:
|
| 598 |
-
pl_name = "my supermix" if "supermix" in song.lower() else "mix"
|
| 599 |
-
|
| 600 |
-
playlists = ytmusic.get_library_playlists(limit=50)
|
| 601 |
-
best_match = None
|
| 602 |
-
|
| 603 |
-
# Find closest match
|
| 604 |
-
for pl in playlists:
|
| 605 |
-
if pl_name in pl.get('title', '').lower() or pl.get('title', '').lower() in pl_name:
|
| 606 |
-
best_match = pl
|
| 607 |
-
break
|
| 608 |
-
|
| 609 |
-
if not best_match and playlists:
|
| 610 |
-
best_match = playlists[0] # fallback to first
|
| 611 |
-
|
| 612 |
-
if best_match:
|
| 613 |
-
tracks = ytmusic.get_playlist(best_match['playlistId'], limit=50)
|
| 614 |
-
video_ids = [track['videoId'] for track in tracks.get('tracks', []) if track.get('videoId')]
|
| 615 |
-
return jsonify({"speak": f"Playing {best_match.get('title')}.", "action": "play_native", "video_ids": video_ids, "title": best_match.get('title')})
|
| 616 |
-
else:
|
| 617 |
-
return jsonify({"speak": "I couldn't find that playlist in your library."})
|
| 618 |
-
|
| 619 |
# Generic search
|
| 620 |
results = ytmusic.search(f"{song} lyrics", filter="videos", limit=10)
|
| 621 |
video_ids = [res['videoId'] for res in results if 'videoId' in res]
|
|
|
|
| 227 |
@app.route('/settings')
|
| 228 |
@login_required
|
| 229 |
def settings():
|
| 230 |
+
return render_template('settings.html')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
@app.route('/')
|
| 233 |
@login_required
|
| 234 |
def index():
|
| 235 |
+
return render_template('index.html')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
|
| 237 |
@app.route('/api/status')
|
| 238 |
def api_status():
|
|
|
|
| 486 |
return jsonify({"speak": "Skipping.", "action": "next_native"})
|
| 487 |
if song:
|
| 488 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
# Inject a Chrome-impersonated session to bypass YouTube bot blocking on Hugging Face Spaces
|
| 490 |
custom_session = cffi_requests.Session(impersonate="chrome")
|
| 491 |
+
ytmusic = YTMusic(requests_session=custom_session)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
# Generic search
|
| 494 |
results = ytmusic.search(f"{song} lyrics", filter="videos", limit=10)
|
| 495 |
video_ids = [res['videoId'] for res in results if 'videoId' in res]
|
templates/index.html
CHANGED
|
@@ -929,33 +929,7 @@
|
|
| 929 |
</aside>
|
| 930 |
</div>
|
| 931 |
|
| 932 |
-
<!-- OAuth Modal -->
|
| 933 |
-
<div id="cookieModal" class="modal-overlay">
|
| 934 |
-
<div class="modal-content">
|
| 935 |
-
<h2 style="font-size: 1.8rem; margin-bottom: 0.5rem; color: var(--primary);"><i class="fa-brands fa-youtube" style="color: #ff0000;"></i> YouTube Music Setup</h2>
|
| 936 |
-
<p style="color: var(--text-muted); margin-bottom: 2rem; font-size: 0.9rem; line-height: 1.5;">Welcome! To play music flawlessly, please link your YouTube Music account.</p>
|
| 937 |
-
|
| 938 |
-
<div id="oauthInitState">
|
| 939 |
-
<button type="button" class="btn-glow" id="startOauthBtn" style="width: 100%; justify-content: center; padding: 1rem 2rem; font-size: 1.1rem; margin-bottom: 1rem;">Link Google Account</button>
|
| 940 |
-
<button type="button" class="btn-glow" id="closeModalBtn" style="width: 100%; justify-content: center; padding: 1rem 2rem; font-size: 1.1rem; background: #333333; color: white; border: 1px solid var(--glass-border);">Skip</button>
|
| 941 |
-
</div>
|
| 942 |
|
| 943 |
-
<div id="oauthCodeState" style="display: none; text-align: center;">
|
| 944 |
-
<p style="color: var(--text-main); margin-bottom: 1rem;">Go to the link below and enter this code:</p>
|
| 945 |
-
<div style="background: rgba(0,0,0,0.5); padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
|
| 946 |
-
<a id="oauthLink" href="#" target="_blank" style="color: var(--primary); font-size: 1.2rem; display: block; margin-bottom: 0.5rem;">google.com/device</a>
|
| 947 |
-
<div style="display: flex; align-items: center; justify-content: center; gap: 15px;">
|
| 948 |
-
<h1 id="oauthCode" style="color: var(--warning); letter-spacing: 5px; font-size: 2rem; margin: 0;"></h1>
|
| 949 |
-
<button type="button" id="copyOauthCodeBtn" style="background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 1.5rem; transition: color 0.2s; padding: 0;" title="Copy code">
|
| 950 |
-
<i class="fa-regular fa-copy"></i>
|
| 951 |
-
</button>
|
| 952 |
-
</div>
|
| 953 |
-
</div>
|
| 954 |
-
<p style="color: var(--text-muted); font-size: 0.85rem; margin-bottom: 1.5rem;"><i class="fa-solid fa-circle-notch fa-spin"></i> Waiting for authorization...</p>
|
| 955 |
-
<button type="button" class="btn-glow" id="cancelOauthBtn" style="width: 100%; justify-content: center; background: #333333; color: white; border: 1px solid var(--glass-border);">Cancel</button>
|
| 956 |
-
</div>
|
| 957 |
-
</div>
|
| 958 |
-
</div>
|
| 959 |
|
| 960 |
<!-- Chat FAB for Mobile -->
|
| 961 |
<button id="chatFab" class="chat-fab" title="Open Chat">
|
|
@@ -1502,102 +1476,7 @@
|
|
| 1502 |
if (e.key === 'Enter') sendTextMessage();
|
| 1503 |
});
|
| 1504 |
|
| 1505 |
-
// OAuth Modal Logic
|
| 1506 |
-
{% if not has_oauth %}
|
| 1507 |
-
document.getElementById('cookieModal').style.display = 'flex';
|
| 1508 |
-
{% endif %}
|
| 1509 |
-
|
| 1510 |
-
let oauthPollInterval = null;
|
| 1511 |
-
|
| 1512 |
-
document.getElementById('closeModalBtn')?.addEventListener('click', () => {
|
| 1513 |
-
document.getElementById('cookieModal').style.display = 'none';
|
| 1514 |
-
});
|
| 1515 |
-
|
| 1516 |
-
document.getElementById('copyOauthCodeBtn').addEventListener('click', function() {
|
| 1517 |
-
const code = document.getElementById('oauthCode').innerText;
|
| 1518 |
-
if (code) {
|
| 1519 |
-
navigator.clipboard.writeText(code).then(() => {
|
| 1520 |
-
const icon = this.querySelector('i');
|
| 1521 |
-
icon.classList.remove('fa-copy', 'fa-regular');
|
| 1522 |
-
icon.classList.add('fa-check', 'fa-solid');
|
| 1523 |
-
icon.style.color = 'var(--success)';
|
| 1524 |
-
setTimeout(() => {
|
| 1525 |
-
icon.classList.remove('fa-check', 'fa-solid');
|
| 1526 |
-
icon.classList.add('fa-copy', 'fa-regular');
|
| 1527 |
-
icon.style.color = '';
|
| 1528 |
-
}, 2000);
|
| 1529 |
-
});
|
| 1530 |
-
}
|
| 1531 |
-
});
|
| 1532 |
-
|
| 1533 |
-
document.getElementById('cancelOauthBtn')?.addEventListener('click', () => {
|
| 1534 |
-
if(oauthPollInterval) clearInterval(oauthPollInterval);
|
| 1535 |
-
document.getElementById('oauthInitState').style.display = 'block';
|
| 1536 |
-
document.getElementById('oauthCodeState').style.display = 'none';
|
| 1537 |
-
});
|
| 1538 |
-
|
| 1539 |
-
document.getElementById('startOauthBtn')?.addEventListener('click', async () => {
|
| 1540 |
-
const btn = document.getElementById('startOauthBtn');
|
| 1541 |
-
const originalText = btn.innerHTML;
|
| 1542 |
-
btn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> Connecting...';
|
| 1543 |
-
btn.disabled = true;
|
| 1544 |
-
|
| 1545 |
-
try {
|
| 1546 |
-
const response = await fetch('/api/oauth/start', { method: 'POST' });
|
| 1547 |
-
const data = await response.json();
|
| 1548 |
-
|
| 1549 |
-
btn.innerHTML = originalText;
|
| 1550 |
-
btn.disabled = false;
|
| 1551 |
-
|
| 1552 |
-
if (data.error) {
|
| 1553 |
-
alert('Error starting OAuth: ' + data.error);
|
| 1554 |
-
return;
|
| 1555 |
-
}
|
| 1556 |
-
|
| 1557 |
-
document.getElementById('oauthLink').href = data.verification_url;
|
| 1558 |
-
document.getElementById('oauthLink').innerText = data.verification_url;
|
| 1559 |
-
document.getElementById('oauthCode').innerText = data.user_code;
|
| 1560 |
-
|
| 1561 |
-
document.getElementById('oauthInitState').style.display = 'none';
|
| 1562 |
-
document.getElementById('oauthCodeState').style.display = 'block';
|
| 1563 |
-
|
| 1564 |
-
// Start polling
|
| 1565 |
-
const deviceCode = data.device_code;
|
| 1566 |
-
const interval = data.interval || 5;
|
| 1567 |
-
|
| 1568 |
-
oauthPollInterval = setInterval(async () => {
|
| 1569 |
-
try {
|
| 1570 |
-
const pollRes = await fetch('/api/oauth/poll', {
|
| 1571 |
-
method: 'POST',
|
| 1572 |
-
headers: { 'Content-Type': 'application/json' },
|
| 1573 |
-
body: JSON.stringify({ device_code: deviceCode })
|
| 1574 |
-
});
|
| 1575 |
-
const pollData = await pollRes.json();
|
| 1576 |
-
|
| 1577 |
-
if (pollData.status === 'success') {
|
| 1578 |
-
clearInterval(oauthPollInterval);
|
| 1579 |
-
alert('Successfully linked YouTube Music!');
|
| 1580 |
-
window.location.reload();
|
| 1581 |
-
} else if (pollData.status === 'expired') {
|
| 1582 |
-
clearInterval(oauthPollInterval);
|
| 1583 |
-
alert('Code expired. Please try again.');
|
| 1584 |
-
document.getElementById('cancelOauthBtn').click();
|
| 1585 |
-
} else if (pollData.error) {
|
| 1586 |
-
clearInterval(oauthPollInterval);
|
| 1587 |
-
alert('Error: ' + pollData.error);
|
| 1588 |
-
document.getElementById('cancelOauthBtn').click();
|
| 1589 |
-
}
|
| 1590 |
-
} catch(err) {
|
| 1591 |
-
console.error('Polling error', err);
|
| 1592 |
-
}
|
| 1593 |
-
}, interval * 1000);
|
| 1594 |
|
| 1595 |
-
} catch(e) {
|
| 1596 |
-
btn.innerHTML = originalText;
|
| 1597 |
-
btn.disabled = false;
|
| 1598 |
-
alert('Connection error');
|
| 1599 |
-
}
|
| 1600 |
-
});
|
| 1601 |
|
| 1602 |
</script>
|
| 1603 |
</body>
|
|
|
|
| 929 |
</aside>
|
| 930 |
</div>
|
| 931 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 932 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 933 |
|
| 934 |
<!-- Chat FAB for Mobile -->
|
| 935 |
<button id="chatFab" class="chat-fab" title="Open Chat">
|
|
|
|
| 1476 |
if (e.key === 'Enter') sendTextMessage();
|
| 1477 |
});
|
| 1478 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1479 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1480 |
|
| 1481 |
</script>
|
| 1482 |
</body>
|
templates/settings.html
CHANGED
|
@@ -175,125 +175,13 @@
|
|
| 175 |
</div>
|
| 176 |
|
| 177 |
<div class="instructions" style="text-align: center;">
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
<p style="margin-top: 10px;">Your YouTube Music account is successfully linked!</p>
|
| 181 |
-
{% else %}
|
| 182 |
-
<h3 style="color: var(--warning);"><i class="fa-solid fa-triangle-exclamation"></i> Account Not Linked</h3>
|
| 183 |
-
<p style="margin-top: 10px;">Link your account to ensure high-quality music streaming.</p>
|
| 184 |
-
{% endif %}
|
| 185 |
</div>
|
| 186 |
|
| 187 |
<div id="oauthInitState">
|
| 188 |
-
<button type="button" class="btn-glow" id="startOauthBtn" style="margin-bottom: 1rem;">
|
| 189 |
-
{% if has_oauth %}Relink Account{% else %}Link Google Account{% endif %}
|
| 190 |
-
</button>
|
| 191 |
<a href="{{ url_for('index') }}" class="btn-glow btn-secondary">Back to Dashboard</a>
|
| 192 |
</div>
|
| 193 |
-
|
| 194 |
-
<div id="oauthCodeState" style="display: none; text-align: center;">
|
| 195 |
-
<p style="color: var(--text-main); margin-bottom: 1rem;">Go to the link below and enter this code:</p>
|
| 196 |
-
<div style="background: rgba(0,0,0,0.5); padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
|
| 197 |
-
<a id="oauthLink" href="#" target="_blank" style="color: var(--primary); font-size: 1.2rem; display: block; margin-bottom: 0.5rem;">google.com/device</a>
|
| 198 |
-
<div style="display: flex; align-items: center; justify-content: center; gap: 15px;">
|
| 199 |
-
<h1 id="oauthCode" style="color: var(--warning); letter-spacing: 5px; font-size: 2rem; margin: 0;"></h1>
|
| 200 |
-
<button type="button" id="copyOauthCodeBtn" style="background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 1.5rem; transition: color 0.2s; padding: 0;" title="Copy code">
|
| 201 |
-
<i class="fa-regular fa-copy"></i>
|
| 202 |
-
</button>
|
| 203 |
-
</div>
|
| 204 |
-
</div>
|
| 205 |
-
<p style="color: var(--text-muted); font-size: 0.85rem; margin-bottom: 1.5rem;"><i class="fa-solid fa-circle-notch fa-spin"></i> Waiting for authorization...</p>
|
| 206 |
-
<button type="button" class="btn-glow btn-secondary" id="cancelOauthBtn">Cancel</button>
|
| 207 |
-
</div>
|
| 208 |
</div>
|
| 209 |
-
|
| 210 |
-
<script>
|
| 211 |
-
let oauthPollInterval = null;
|
| 212 |
-
|
| 213 |
-
document.getElementById('copyOauthCodeBtn').addEventListener('click', function() {
|
| 214 |
-
const code = document.getElementById('oauthCode').innerText;
|
| 215 |
-
if (code) {
|
| 216 |
-
navigator.clipboard.writeText(code).then(() => {
|
| 217 |
-
const icon = this.querySelector('i');
|
| 218 |
-
icon.classList.remove('fa-copy', 'fa-regular');
|
| 219 |
-
icon.classList.add('fa-check', 'fa-solid');
|
| 220 |
-
icon.style.color = 'var(--success)';
|
| 221 |
-
setTimeout(() => {
|
| 222 |
-
icon.classList.remove('fa-check', 'fa-solid');
|
| 223 |
-
icon.classList.add('fa-copy', 'fa-regular');
|
| 224 |
-
icon.style.color = '';
|
| 225 |
-
}, 2000);
|
| 226 |
-
});
|
| 227 |
-
}
|
| 228 |
-
});
|
| 229 |
-
|
| 230 |
-
document.getElementById('cancelOauthBtn')?.addEventListener('click', () => {
|
| 231 |
-
if(oauthPollInterval) clearInterval(oauthPollInterval);
|
| 232 |
-
document.getElementById('oauthInitState').style.display = 'block';
|
| 233 |
-
document.getElementById('oauthCodeState').style.display = 'none';
|
| 234 |
-
});
|
| 235 |
-
|
| 236 |
-
document.getElementById('startOauthBtn')?.addEventListener('click', async () => {
|
| 237 |
-
const btn = document.getElementById('startOauthBtn');
|
| 238 |
-
const originalText = btn.innerHTML;
|
| 239 |
-
btn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> Connecting...';
|
| 240 |
-
btn.disabled = true;
|
| 241 |
-
|
| 242 |
-
try {
|
| 243 |
-
const response = await fetch('/api/oauth/start', { method: 'POST' });
|
| 244 |
-
const data = await response.json();
|
| 245 |
-
|
| 246 |
-
btn.innerHTML = originalText;
|
| 247 |
-
btn.disabled = false;
|
| 248 |
-
|
| 249 |
-
if (data.error) {
|
| 250 |
-
alert('Error starting OAuth: ' + data.error);
|
| 251 |
-
return;
|
| 252 |
-
}
|
| 253 |
-
|
| 254 |
-
document.getElementById('oauthLink').href = data.verification_url;
|
| 255 |
-
document.getElementById('oauthLink').innerText = data.verification_url;
|
| 256 |
-
document.getElementById('oauthCode').innerText = data.user_code;
|
| 257 |
-
|
| 258 |
-
document.getElementById('oauthInitState').style.display = 'none';
|
| 259 |
-
document.getElementById('oauthCodeState').style.display = 'block';
|
| 260 |
-
|
| 261 |
-
const deviceCode = data.device_code;
|
| 262 |
-
const interval = data.interval || 5;
|
| 263 |
-
|
| 264 |
-
oauthPollInterval = setInterval(async () => {
|
| 265 |
-
try {
|
| 266 |
-
const pollRes = await fetch('/api/oauth/poll', {
|
| 267 |
-
method: 'POST',
|
| 268 |
-
headers: { 'Content-Type': 'application/json' },
|
| 269 |
-
body: JSON.stringify({ device_code: deviceCode })
|
| 270 |
-
});
|
| 271 |
-
const pollData = await pollRes.json();
|
| 272 |
-
|
| 273 |
-
if (pollData.status === 'success') {
|
| 274 |
-
clearInterval(oauthPollInterval);
|
| 275 |
-
alert('Successfully linked YouTube Music!');
|
| 276 |
-
window.location.reload();
|
| 277 |
-
} else if (pollData.status === 'expired') {
|
| 278 |
-
clearInterval(oauthPollInterval);
|
| 279 |
-
alert('Code expired. Please try again.');
|
| 280 |
-
document.getElementById('cancelOauthBtn').click();
|
| 281 |
-
} else if (pollData.error) {
|
| 282 |
-
clearInterval(oauthPollInterval);
|
| 283 |
-
alert('Error: ' + pollData.error);
|
| 284 |
-
document.getElementById('cancelOauthBtn').click();
|
| 285 |
-
}
|
| 286 |
-
} catch(err) {
|
| 287 |
-
console.error('Polling error', err);
|
| 288 |
-
}
|
| 289 |
-
}, interval * 1000);
|
| 290 |
-
|
| 291 |
-
} catch(e) {
|
| 292 |
-
btn.innerHTML = originalText;
|
| 293 |
-
btn.disabled = false;
|
| 294 |
-
alert('Connection error');
|
| 295 |
-
}
|
| 296 |
-
});
|
| 297 |
-
</script>
|
| 298 |
</body>
|
| 299 |
</html>
|
|
|
|
| 175 |
</div>
|
| 176 |
|
| 177 |
<div class="instructions" style="text-align: center;">
|
| 178 |
+
<h3 style="color: var(--text-main);"><i class="fa-solid fa-info-circle"></i> Settings</h3>
|
| 179 |
+
<p style="margin-top: 10px;">OAuth has been completely removed from the system. No configuration is required.</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
</div>
|
| 181 |
|
| 182 |
<div id="oauthInitState">
|
|
|
|
|
|
|
|
|
|
| 183 |
<a href="{{ url_for('index') }}" class="btn-glow btn-secondary">Back to Dashboard</a>
|
| 184 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
</body>
|
| 187 |
</html>
|