feat: migrate to PostgreSQL and enhance playlist curation

- Migrate database from SQLite to PostgreSQL (100.91.248.114:5433)
- Fix playlist curation to use actual top tracks instead of AI name matching
- Add /playlists/history endpoint for historical playlist viewing
- Add Playlist Archives section to frontend with expandable history
- Add playlist-modify-* scopes to Spotify OAuth for playlist creation
- Rewrite Genius client to use official API (fixes 403 scraping blocks)
- Ensure playlists are created on Spotify before curation attempts
- Add DATABASE.md documentation for PostgreSQL schema
- Add migrations for PlaylistConfig and composition storage
This commit is contained in:
bnair123
2025-12-30 22:24:56 +04:00
parent 26b4895695
commit 272148c5bf
19 changed files with 1130 additions and 145 deletions

View File

@@ -1,12 +1,14 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Card, Button, Typography, Space, Spin, message, Tooltip as AntTooltip } from 'antd';
import { Card, Button, Typography, Space, Spin, message, Tooltip as AntTooltip, Collapse, Empty } from 'antd';
import {
PlayCircleOutlined,
ReloadOutlined,
HistoryOutlined,
InfoCircleOutlined,
CustomerServiceOutlined
CustomerServiceOutlined,
CalendarOutlined,
DownOutlined
} from '@ant-design/icons';
import Tooltip from './Tooltip';
import TrackList from './TrackList';
@@ -17,6 +19,10 @@ const PlaylistsSection = () => {
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState({ sixHour: false, daily: false });
const [playlists, setPlaylists] = useState(null);
const [history, setHistory] = useState([]);
const [loadingHistory, setLoadingHistory] = useState(false);
const [showHistory, setShowHistory] = useState(false);
const fetchPlaylists = async () => {
try {
@@ -30,10 +36,30 @@ const PlaylistsSection = () => {
}
};
const fetchHistory = async () => {
if (loadingHistory) return;
setLoadingHistory(true);
try {
const response = await axios.get('/api/playlists/history');
setHistory(response.data.history || []);
} catch (error) {
console.error('Failed to fetch playlist history:', error);
message.error('Failed to load playlist history');
} finally {
setLoadingHistory(false);
}
};
useEffect(() => {
fetchPlaylists();
}, []);
useEffect(() => {
if (showHistory && history.length === 0) {
fetchHistory();
}
}, [showHistory]);
const handleRefresh = async (type) => {
const isSixHour = type === 'six-hour';
setRefreshing(prev => ({ ...prev, [isSixHour ? 'sixHour' : 'daily']: true }));
@@ -43,6 +69,7 @@ const PlaylistsSection = () => {
await axios.post(endpoint);
message.success(`${isSixHour ? '6-Hour' : 'Daily'} playlist refreshed!`);
await fetchPlaylists();
if (showHistory) fetchHistory();
} catch (error) {
console.error(`Refresh failed for ${type}:`, error);
message.error(`Failed to refresh ${type} playlist`);
@@ -53,6 +80,32 @@ const PlaylistsSection = () => {
if (loading) return <div className="flex justify-center p-8"><Spin size="large" /></div>;
const historyItems = history.map((item) => ({
key: item.id,
label: (
<div className="flex justify-between items-center w-full">
<div className="flex items-center space-x-3">
<Text className="text-gray-400 font-mono text-xs">
{new Date(item.date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
</Text>
<Text className="text-white font-medium">{item.theme}</Text>
<span className="px-2 py-0.5 rounded text-[10px] bg-slate-700 text-blue-300 border border-slate-600">
{item.period_label || '6h'}
</span>
</div>
<Text className="text-gray-500 text-xs">{item.composition?.length || 0} tracks</Text>
</div>
),
children: (
<div className="pl-2">
<Paragraph className="text-gray-300 text-sm italic mb-2 border-l-2 border-blue-500 pl-3 py-1">
"{item.reasoning}"
</Paragraph>
<TrackList tracks={item.composition} maxHeight="max-h-96" />
</div>
),
}));
return (
<div className="mt-8 space-y-6">
<div className="flex items-center space-x-2">
@@ -162,6 +215,39 @@ const PlaylistsSection = () => {
</div>
</Card>
</div>
<div className="mt-8 border-t border-slate-700 pt-6">
<Button
type="text"
onClick={() => setShowHistory(!showHistory)}
className="flex items-center text-gray-400 hover:text-white p-0 text-lg font-medium mb-4 transition-colors"
>
<CalendarOutlined className="mr-2" />
Playlist Archives
<DownOutlined className={`ml-2 text-xs transition-transform duration-300 ${showHistory ? 'rotate-180' : ''}`} />
</Button>
{showHistory && (
<div className="animate-fade-in">
{loadingHistory ? (
<div className="flex justify-center p-8"><Spin /></div>
) : history.length > 0 ? (
<Collapse
items={historyItems}
bordered={false}
className="bg-transparent"
expandIconPosition="end"
ghost
theme="dark"
itemLayout="horizontal"
style={{ background: 'transparent' }}
/>
) : (
<Empty description={<span className="text-gray-500">No playlist history available yet</span>} />
)}
</div>
)}
</div>
</div>
);
};