Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>

This commit is contained in:
mbrucedogs 2025-07-26 14:44:36 -05:00
parent ca36e2dbf5
commit 668bb0a307
10 changed files with 707 additions and 243 deletions

View File

@ -1,6 +1,6 @@
# Music Charts Archive Scraper
# Multi-Source Music Charts Analytics Platform
A full-stack React application for scraping and visualizing music chart data from the Music Charts Archive website.
A full-stack React application for scraping and visualizing music chart data from multiple sources including Music Charts Archive, Kworb, and Shazam.
## Project Structure
@ -22,6 +22,9 @@ MusicCharts/
│ ├── routes/ # API routes
│ ├── controllers/ # Route controllers
│ ├── models/ # Data models & scraping logic
│ │ ├── chartService.js # Music Charts Archive
│ │ ├── kworbService.js # Kworb integration
│ │ └── shazamService.js # Shazam integration
│ ├── middleware/ # Express middleware
│ └── server.js # Main server file
└── package.json
@ -29,7 +32,15 @@ MusicCharts/
## Features
- **Year Selection**: Choose different years to view available chart dates
### **Multi-Source Data Integration**
- **Music Charts Archive**: Historical Billboard charts with weekly and yearly data
- **Kworb**: Real-time chart data with current trending information
- **Shazam**: Current trending songs with multiple chart types (no historical data)
- **Source Switching**: Seamless switching between data sources via tab interface
- **Chart Type Selection**: Multiple chart types for Shazam integration
### **Core Functionality**
- **Year Selection**: Choose different years to view available chart dates (Music Charts Archive & Kworb)
- **Date Selection**: Browse and select specific chart dates for detailed song data
- **Music Chart Display**: View ranked songs with title and artist information
- **Yearly Analytics**: Calculate and display top songs of the year using multi-factor ranking algorithm
@ -38,12 +49,13 @@ MusicCharts/
- **Property Selection**: Choose which data properties to include in exports
- **Responsive Design**: Mobile-friendly interface
- **Error Handling**: Comprehensive error states and loading indicators
- **Web Scraping**: Real-time data scraping from Music Charts Archive
- **Web Scraping**: Real-time data scraping from multiple sources
## Frontend Components
- `YearSelector`: Dropdown for year selection
- `DateList`: Grid of available chart dates for selected year
- `SourceSelector`: Tab-based interface for switching between data sources
- `YearSelector`: Dropdown for year selection (conditional based on source)
- `DateList`: Grid of available chart dates for selected year (conditional)
- `ChartTable`: Music chart data table with rankings
- `YearlyTopSongs`: Enhanced yearly analytics display with calculation metrics
- `JsonViewer`: Interactive JSON data viewer with property selection
@ -54,9 +66,10 @@ MusicCharts/
## Backend API Endpoints
- `GET /api/health` - Health check endpoint
- `GET /api/chart/dates/:year` - Get available chart dates for a year
- `GET /api/chart/data/:date` - Get chart data for a specific date
- `GET /api/chart/yearly-top/:year` - Get yearly top songs ranking with calculation metrics
- `GET /api/chart/dates/:year` - Get available chart dates for a year (with source query param)
- `GET /api/chart/data/:date` - Get chart data for a specific date (with source and chartType query params)
- `GET /api/chart/yearly-top/:year` - Get yearly top songs ranking with calculation metrics (with source and chartType query params)
- `GET /api/chart/shazam/chart-types` - Get available Shazam chart types
## Setup Instructions
@ -82,6 +95,7 @@ MusicCharts/
PORT=3001
NODE_ENV=development
CORS_ORIGIN=http://localhost:3000
SHAZAM_API_KEY=your_shazam_api_key_here
```
4. Start the development server:
@ -114,11 +128,12 @@ The frontend will be available at `http://localhost:3000`
### Get Available Chart Dates
```
GET /api/chart/dates/:year
GET /api/chart/dates/:year?source=musicchartsarchive
```
**Parameters:**
- `year` (number): The year to get chart dates for (1970-present)
- `source` (query): Data source - 'musicchartsarchive', 'kworb', or 'shazam'
**Response:**
```json
@ -136,11 +151,13 @@ GET /api/chart/dates/:year
### Get Chart Data
```
GET /api/chart/data/:date
GET /api/chart/data/:date?source=musicchartsarchive&chartType=top-200
```
**Parameters:**
- `date` (string): Date in YYYY-MM-DD format
- `date` (string): Date in YYYY-MM-DD format (or 'today' for Shazam)
- `source` (query): Data source - 'musicchartsarchive', 'kworb', or 'shazam'
- `chartType` (query): Chart type for Shazam (e.g., 'top-200', 'top-100')
**Response:**
```json
@ -160,11 +177,13 @@ GET /api/chart/data/:date
### Get Yearly Top Songs
```
GET /api/chart/yearly-top/:year
GET /api/chart/yearly-top/:year?source=musicchartsarchive&chartType=top-200
```
**Parameters:**
- `year` (number): The year to get yearly top songs for
- `source` (query): Data source - 'musicchartsarchive', 'kworb', or 'shazam'
- `chartType` (query): Chart type for Shazam
**Response:**
```json
@ -188,11 +207,52 @@ GET /api/chart/yearly-top/:year
]
```
## Data Source
### Get Shazam Chart Types
```
GET /api/chart/shazam/chart-types
```
This application scrapes data from [Music Charts Archive](https://musicchartsarchive.com), a comprehensive database of historical music charts. The scraper extracts:
**Response:**
```json
[
{
"id": "top-200",
"name": "Top 200"
},
{
"id": "top-100",
"name": "Top 100"
}
]
```
1. **Chart Dates**: Available chart dates for each year
## Data Sources
This application integrates with multiple music chart data sources:
### **Music Charts Archive**
- **URL**: https://musicchartsarchive.com
- **Data**: Historical Billboard charts
- **Features**: Weekly and yearly data, comprehensive historical records
- **Scraping**: HTML parsing with Cheerio
### **Kworb**
- **URL**: https://kworb.net
- **Data**: Real-time chart data
- **Features**: Current trending information, historical support
- **Scraping**: HTML parsing with Cheerio
### **Shazam**
- **URL**: https://www.shazam.com
- **Data**: Current trending songs
- **Features**: Multiple chart types, real-time data only (no historical)
- **Integration**: Official API with authentication
## Data Processing
The application extracts and processes:
1. **Chart Dates**: Available chart dates for each year (Music Charts Archive & Kworb)
2. **Song Rankings**: Top songs with their chart positions, titles, and artists
3. **Yearly Analytics**: Multi-factor ranking algorithm calculating:
- **Total Points**: Reverse scoring (1st = 50 points, 50th = 1 point)
@ -205,7 +265,7 @@ This application scrapes data from [Music Charts Archive](https://musicchartsarc
1. **Frontend**: Add new components in `frontend/src/components/`
2. **Backend**: Add new routes in `backend/src/routes/` and controllers in `backend/src/controllers/`
3. **Scraping**: Modify `backend/src/models/chartService.js` for new data extraction
3. **Scraping**: Modify service files in `backend/src/models/` for new data extraction
4. **Styling**: Modify `frontend/src/styles.css` for global styles
5. **API Integration**: Update `frontend/src/services/chartApi.js` for new endpoints
6. **JSON Formatting**: Modify `frontend/src/components/JsonViewer.jsx` for custom data formats
@ -243,10 +303,11 @@ This application scrapes data from [Music Charts Archive](https://musicchartsarc
- **Progress Logging**: Real-time progress updates during yearly calculations
- **Error Recovery**: Graceful handling of failed requests with continuation
- **Memory Optimization**: Efficient data structures for large datasets
- **Multi-Source Routing**: Efficient routing to appropriate service based on source
## Legal Notice
This application is for educational and research purposes. Please respect the terms of service of the Music Charts Archive website and implement appropriate rate limiting and caching strategies for production use.
This application is for educational and research purposes. Please respect the terms of service of all data sources (Music Charts Archive, Kworb, and Shazam) and implement appropriate rate limiting and caching strategies for production use.
## License

View File

@ -1,46 +1,56 @@
# Product Requirements Document (PRD)
## Music Charts Archive Scraper & Analytics Platform
## Multi-Source Music Charts Analytics Platform
---
## 📋 **Executive Summary**
A full-stack web application that scrapes, analyzes, and visualizes music chart data from the Music Charts Archive website. The platform provides both weekly chart views and comprehensive yearly rankings, enabling users to explore historical music trends and download data for further analysis.
A full-stack web application that scrapes, analyzes, and visualizes music chart data from multiple sources including Music Charts Archive, Kworb, and Shazam. The platform provides both weekly chart views and comprehensive yearly rankings, enabling users to explore historical music trends and download data for further analysis across different data sources.
---
## 🎯 **Product Vision**
Create a comprehensive music analytics platform that transforms raw chart data into actionable insights, making historical music trends accessible and analyzable for researchers, music enthusiasts, and data analysts.
Create a comprehensive music analytics platform that transforms raw chart data from multiple sources into actionable insights, making historical music trends accessible and analyzable for researchers, music enthusiasts, and data analysts.
---
## 🎵 **Core Features**
### **1. Data Scraping Engine**
### **1. Multi-Source Data Integration**
- **Music Charts Archive**: Historical Billboard charts with weekly and yearly data
- **Kworb**: Real-time chart data with current trending information
- **Shazam**: Current trending songs with multiple chart types (no historical data)
- **Source Switching**: Seamless switching between data sources
- **Chart Type Selection**: Multiple chart types for Shazam integration
### **2. Data Scraping Engine**
- **Weekly Chart Scraping**: Extract song rankings from individual chart dates
- **Yearly Data Aggregation**: Collect and analyze data across all available dates for a year
- **Real-time Data Fetching**: Live scraping from Music Charts Archive website
- **Real-time Data Fetching**: Live scraping from multiple sources
- **Error Handling**: Graceful handling of missing data and network issues
### **2. Weekly Chart View**
- **Date Selection**: Browse available chart dates by year
- **Chart Display**: View top 50 songs with rankings, titles, and artists
### **3. Weekly Chart View**
- **Source Selection**: Choose between Music Charts Archive, Kworb, or Shazam
- **Date Selection**: Browse available chart dates by year (Music Charts Archive & Kworb)
- **Chart Display**: View top songs with rankings, titles, and artists
- **Data Export**: Download weekly chart data in JSON format
- **Responsive Design**: Mobile and desktop optimized interface
### **3. Yearly Analytics**
### **4. Yearly Analytics**
- **Yearly Rankings**: Calculate top songs of the year based on:
- Total points (position-based scoring)
- Highest achieved position
- Number of appearances
- **Smart Algorithm**: Multi-factor ranking system
- **Yearly Export**: Download comprehensive yearly data
- **Source-Specific Logic**: Different algorithms for different data sources
### **4. Data Export System**
### **5. Data Export System**
- **JSON Format**: Structured data export with metadata
- **File Naming**: Date/year-based file naming convention
- **Metadata Inclusion**: Chart date, total songs, formatted titles
- **Source Attribution**: Data source information in exports
---
@ -62,18 +72,22 @@ GET /api/health
GET /api/chart/dates/:year
- Purpose: Get available chart dates for a year
- Parameters: year (number)
- Parameters: year (number), source (query param)
- Response: Array of date objects with date and formattedDate
GET /api/chart/data/:date
- Purpose: Get chart data for specific date
- Parameters: date (YYYY-MM-DD format)
- Parameters: date (YYYY-MM-DD format), source (query param), chartType (query param for Shazam)
- Response: Array of song objects with order, title, artist
GET /api/chart/yearly-top/:year
- Purpose: Get yearly top songs ranking with calculation metrics
- Parameters: year (number)
- Response: Array of top 50 songs with order, title, artist, totalPoints, highestPosition, appearances
- Parameters: year (number), source (query param), chartType (query param for Shazam)
- Response: Array of top songs with order, title, artist, totalPoints, highestPosition, appearances
GET /api/chart/shazam/chart-types
- Purpose: Get available Shazam chart types
- Response: Array of chart type objects with id and name
```
#### **Data Models**
@ -115,17 +129,32 @@ GET /api/chart/yearly-top/:year
highestPosition: 1,
appearances: 8
}
// Chart Type Object (Shazam)
{
id: "top-200",
name: "Top 200"
}
```
#### **Service Architecture**
```javascript
// Service Layer Structure
ChartService - Music Charts Archive integration
KworbService - Kworb real-time data integration
ShazamService - Shazam API integration with chart types
```
#### **Scraping Logic**
1. **Date Extraction**: Parse HTML for chart date links
2. **Song Data Extraction**: Parse table rows for song information
3. **Data Validation**: Ensure data integrity and completeness
4. **Error Recovery**: Skip problematic dates and continue processing
1. **Source Detection**: Route requests to appropriate service based on source parameter
2. **Date Extraction**: Parse HTML for chart date links (Music Charts Archive & Kworb)
3. **Song Data Extraction**: Parse table rows for song information
4. **Data Validation**: Ensure data integrity and completeness
5. **Error Recovery**: Skip problematic dates and continue processing
#### **Yearly Ranking Algorithm**
```javascript
// Scoring System
// Scoring System (Music Charts Archive & Kworb)
- Position Points: 1st = 50 points, 50th = 1 point
- Highest Position: Track best position achieved
- Appearance Count: Number of weeks on charts
@ -134,6 +163,10 @@ GET /api/chart/yearly-top/:year
1. Total Points (descending)
2. Highest Position (ascending)
3. Appearance Count (descending)
// Shazam Algorithm (Current data only)
- Position-based ranking for current trending songs
- No historical aggregation available
```
### **Frontend Requirements**
@ -148,9 +181,10 @@ GET /api/chart/yearly-top/:year
```
App
├── Layout
├── YearSelector
├── SourceSelector
├── YearSelector (conditional)
├── ViewModeSelector (Weekly/Yearly)
├── DateList (Weekly mode)
├── DateList (Weekly mode, conditional)
├── ChartTable
├── YearlyTopSongs
├── JsonViewer
@ -162,12 +196,15 @@ App
```javascript
// Core State
{
selectedSource: 'musicchartsarchive' | 'kworb' | 'shazam',
selectedYear: number,
selectedDate: string | null,
selectedChartType: string, // For Shazam
viewMode: 'weekly' | 'yearly',
dates: DateObject[],
chartData: SongObject[],
yearlySongs: SongObject[],
shazamChartTypes: ChartTypeObject[],
selectedProperties: {
position: boolean,
title: boolean,
@ -182,18 +219,22 @@ App
{
datesLoading: boolean,
dataLoading: boolean,
yearlyLoading: boolean
yearlyLoading: boolean,
chartTypesLoading: boolean
}
// Error States
{
datesError: string | null,
dataError: string | null,
yearlyError: string | null
yearlyError: string | null,
chartTypesError: string | null
}
```
#### **User Interface Requirements**
- **Source Selection**: Tab-based interface for switching between data sources
- **Conditional UI**: Show/hide components based on selected source
- **Responsive Design**: Mobile-first approach
- **Loading States**: Visual feedback during data fetching with progress indicators
- **Error Handling**: User-friendly error messages with retry options
@ -220,14 +261,21 @@ struct Song: Codable {
let artist: String
}
struct ChartType: Codable {
let id: String
let name: String
}
// Network Layer
class ChartAPIService {
func fetchChartDates(year: Int) async throws -> [ChartDate]
func fetchChartData(date: String) async throws -> [Song]
func fetchYearlyTopSongs(year: Int) async throws -> [Song]
func fetchChartDates(year: Int, source: String) async throws -> [ChartDate]
func fetchChartData(date: String, source: String, chartType: String?) async throws -> [Song]
func fetchYearlyTopSongs(year: Int, source: String, chartType: String?) async throws -> [Song]
func fetchShazamChartTypes() async throws -> [ChartType]
}
// UI Components
struct SourceSelector: View
struct YearSelector: View
struct ChartTableView: View
struct YearlyTopSongsView: View
@ -248,14 +296,21 @@ data class Song(
val artist: String
)
data class ChartType(
val id: String,
val name: String
)
// Network Layer
class ChartAPIService {
suspend fun fetchChartDates(year: Int): List<ChartDate>
suspend fun fetchChartData(date: String): List<Song>
suspend fun fetchYearlyTopSongs(year: Int): List<Song>
suspend fun fetchChartDates(year: Int, source: String): List<ChartDate>
suspend fun fetchChartData(date: String, source: String, chartType: String?): List<Song>
suspend fun fetchYearlyTopSongs(year: Int, source: String, chartType: String?): List<Song>
suspend fun fetchShazamChartTypes(): List<ChartType>
}
// UI Components
@Composable fun SourceSelector()
@Composable fun YearSelector()
@Composable fun ChartTable()
@Composable fun YearlyTopSongs()
@ -266,14 +321,19 @@ class ChartAPIService {
## 🎵 **Data Flow**
### **Source Selection Flow**
1. User selects data source → Load source-specific UI components
2. For Shazam → Fetch available chart types
3. For Music Charts Archive/Kworb → Show year selector and date list
### **Weekly Chart Flow**
1. User selects year → Fetch available dates
1. User selects source and year → Fetch available dates (if applicable)
2. User selects date → Fetch chart data
3. Display chart table → Enable download
4. User downloads → Generate JSON file
### **Yearly Chart Flow**
1. User selects year → Fetch all dates for year
1. User selects source and year → Fetch all dates for year (if applicable)
2. System scrapes all dates → Calculate rankings
3. Display yearly top songs → Enable download
4. User downloads → Generate yearly JSON file
@ -283,11 +343,12 @@ class ChartAPIService {
## 🛠 **Development Guidelines**
### **Backend Development**
1. **Error Handling**: Implement comprehensive error handling
2. **Rate Limiting**: Respect website terms of service
3. **Caching**: Implement caching for frequently accessed data
4. **Logging**: Comprehensive logging for debugging
5. **Testing**: Unit tests for core algorithms
1. **Service Architecture**: Modular service layer for each data source
2. **Error Handling**: Implement comprehensive error handling
3. **Rate Limiting**: Respect website terms of service
4. **Caching**: Implement caching for frequently accessed data
5. **Logging**: Comprehensive logging for debugging
6. **Testing**: Unit tests for core algorithms
### **Frontend Development**
1. **Component Reusability**: Create reusable UI components
@ -295,6 +356,7 @@ class ChartAPIService {
3. **Performance**: Optimize for large datasets
4. **User Experience**: Smooth loading and error states
5. **Accessibility**: Screen reader and keyboard navigation support
6. **Conditional Rendering**: Handle different UI states based on source
### **Mobile Development**
1. **Offline Support**: Cache data for offline viewing
@ -312,12 +374,14 @@ class ChartAPIService {
- **Error Rate**: < 5% failed requests
- **Uptime**: > 99% availability
- **Timeout Handling**: Graceful handling of long-running operations
- **Multi-Source Reliability**: Consistent performance across all data sources
### **User Experience Metrics**
- **Page Load Time**: < 3 seconds
- **User Engagement**: Time spent on platform
- **Download Rate**: Percentage of users downloading data
- **Error Recovery**: Successful error resolution rate
- **Source Switching**: Seamless transitions between data sources
---
@ -328,11 +392,12 @@ class ChartAPIService {
- **User Agent**: Proper identification in requests
- **Error Handling**: No sensitive data in error messages
- **CORS**: Proper cross-origin resource sharing
- **API Key Management**: Secure handling of Shazam API keys
### **Legal Considerations**
- **Terms of Service**: Respect Music Charts Archive terms
- **Terms of Service**: Respect all data source terms
- **Data Usage**: Educational and research purposes
- **Attribution**: Proper credit to data source
- **Attribution**: Proper credit to data sources
- **Rate Limiting**: Responsible scraping practices
---
@ -346,12 +411,14 @@ class ChartAPIService {
- **Advanced Filtering**: Date range, artist, song title filters
- **Enhanced JSON Export**: Customizable data formats and property selection
- **Real-time Progress Tracking**: Visual progress indicators for long operations
- **Data Source Comparison**: Side-by-side comparison of different sources
### **Phase 3 Features**
- **User Accounts**: Save favorite charts and analyses
- **Social Features**: Share charts and insights
- **API Access**: Public API for developers
- **Data Visualization**: Interactive charts and graphs
- **Additional Sources**: Integration with more music data providers
---
@ -360,9 +427,10 @@ class ChartAPIService {
### **Backend Setup**
- [ ] Choose server framework and language
- [ ] Set up web scraping library
- [ ] Implement API endpoints
- [ ] Create data models
- [ ] Implement yearly ranking algorithm
- [ ] Implement multi-source service architecture
- [ ] Create API endpoints with source routing
- [ ] Implement data models for all sources
- [ ] Create yearly ranking algorithms
- [ ] Add error handling and logging
- [ ] Set up testing framework
@ -370,6 +438,8 @@ class ChartAPIService {
- [ ] Choose UI framework
- [ ] Set up state management
- [ ] Create reusable components
- [ ] Implement source selector component
- [ ] Create conditional UI logic
- [ ] Implement API integration
- [ ] Add download functionality
- [ ] Implement responsive design
@ -377,9 +447,9 @@ class ChartAPIService {
### **Mobile Setup (if applicable)**
- [ ] Set up mobile development environment
- [ ] Create data models
- [ ] Implement network layer
- [ ] Create UI components
- [ ] Create data models for all sources
- [ ] Implement network layer with source routing
- [ ] Create UI components with conditional rendering
- [ ] Add native features (download, share)
- [ ] Test on devices
@ -395,7 +465,7 @@ class ChartAPIService {
### **Project Structure**
```
m/
MusicCharts/
├── frontend/ # React frontend application
│ ├── src/
│ │ ├── components/ # React components
@ -411,6 +481,9 @@ m/
│ ├── routes/ # API routes
│ ├── controllers/ # Route controllers
│ ├── models/ # Data models & scraping logic
│ │ ├── chartService.js # Music Charts Archive
│ │ ├── kworbService.js # Kworb integration
│ │ └── shazamService.js # Shazam integration
│ ├── middleware/ # Express middleware
│ └── server.js # Main server file
└── package.json
@ -427,29 +500,35 @@ m/
## 🚀 **Recent Enhancements (Latest Update)**
### **Enhanced Data Export System**
- **JsonViewer Component**: Interactive JSON data inspection with property selection
- **Customizable Exports**: Choose which properties to include in JSON output
- **Copy/Download Functionality**: One-click copy to clipboard and direct download
- **Format Consistency**: Matches reference format from `docs/songList.json`
### **Multi-Source Architecture**
- **SourceSelector Component**: Tab-based interface for switching between data sources
- **Service Layer**: Modular architecture with separate services for each data source
- **Conditional UI**: Dynamic component rendering based on selected source
- **Enhanced API**: Source-aware endpoints with query parameters
### **Performance Optimizations**
- **Extended Timeouts**: 5-minute frontend timeout for yearly calculations
- **Request Delays**: 100ms delays between scraping requests
- **Progress Logging**: Real-time progress updates during long operations
- **Error Recovery**: Graceful handling of failed requests with continuation
### **Shazam Integration**
- **Chart Types**: Multiple chart type selection (Top 200, Top 100, etc.)
- **Current Data**: Real-time trending songs (no historical data)
- **API Integration**: Complete Shazam API integration
- **Chart Type Endpoint**: New endpoint for fetching available chart types
### **Kworb Integration**
- **Real-time Data**: Current chart data from Kworb
- **Historical Support**: Yearly data aggregation capabilities
- **Service Implementation**: Dedicated KworbService for data extraction
### **Enhanced User Experience**
- **Property Selection**: Checkboxes to customize JSON output properties
- **Loading Indicators**: Informative loading messages for long-running operations
- **Enhanced Error Handling**: Better error messages and recovery options
- **Source-Specific UI**: Different interfaces for different data sources
- **Loading States**: Source-aware loading indicators
- **Error Handling**: Source-specific error messages
- **Responsive Design**: Improved mobile and desktop experience
### **Data Format Standards**
- **Consistent JSON Structure**: Standardized format matching industry standards
- **Consistent JSON Structure**: Standardized format across all sources
- **Source Attribution**: Data source information in exports
- **Calculation Properties**: Full visibility into ranking algorithm metrics
- **Flexible Export Options**: Multiple export formats for different use cases
---
This PRD provides a comprehensive blueprint for recreating the Music Charts Archive Scraper in any technology stack, from web frameworks to mobile platforms. The modular architecture and clear data flow make it adaptable to different implementation approaches while maintaining the core functionality and user experience.
This PRD provides a comprehensive blueprint for the multi-source Music Charts Analytics Platform, supporting Music Charts Archive, Kworb, and Shazam integrations. The modular architecture and clear data flow make it adaptable to different implementation approaches while maintaining the core functionality and user experience across multiple data sources.

View File

@ -18,6 +18,7 @@ function App() {
const [viewMode, setViewMode] = useState('weekly'); // 'weekly' or 'yearly'
const [selectedSource, setSelectedSource] = useState('musicchartsarchive');
const [selectedChartType, setSelectedChartType] = useState('top-200');
const [itemCount, setItemCount] = useState(50); // Default to 50 items
const { chartTypes: shazamChartTypes, loading: chartTypesLoading } = useShazamChartTypes();
const { dates, loading: datesLoading, error: datesError } = useChartDates(selectedSource === 'shazam' ? null : selectedYear, selectedSource);
@ -53,6 +54,10 @@ function App() {
setSelectedDate(null); // Reset selected date when chart type changes
};
const handleItemCountChange = (count) => {
setItemCount(count);
};
return (
<Layout>
<div className="app">
@ -125,10 +130,19 @@ function App() {
loading={dataLoading}
error={dataError}
selectedDate={selectedSource === 'shazam' ? 'Current' : selectedDate}
selectedSource={selectedSource}
itemCount={itemCount}
onItemCountChange={handleItemCountChange}
/>
{chartData && chartData.length > 0 && (
<DownloadButton data={chartData} selectedDate={selectedSource === 'shazam' ? 'Current' : selectedDate} />
<DownloadButton
data={chartData}
selectedDate={selectedSource === 'shazam' ? 'Current' : selectedDate}
selectedSource={selectedSource}
itemCount={itemCount}
onItemCountChange={handleItemCountChange}
/>
)}
</>
) : (
@ -138,10 +152,19 @@ function App() {
loading={yearlyLoading}
error={yearlyError}
year={selectedSource === 'shazam' ? 'Current' : selectedYear}
selectedSource={selectedSource}
itemCount={itemCount}
onItemCountChange={handleItemCountChange}
/>
{yearlySongs && yearlySongs.length > 0 && (
<YearlyDownloadButton data={yearlySongs} year={selectedSource === 'shazam' ? 'Current' : selectedYear} />
<YearlyDownloadButton
data={yearlySongs}
year={selectedSource === 'shazam' ? 'Current' : selectedYear}
selectedSource={selectedSource}
itemCount={itemCount}
onItemCountChange={handleItemCountChange}
/>
)}
</>
)}

View File

@ -1,6 +1,7 @@
import React from 'react';
import JsonViewer from './JsonViewer';
const ChartTable = ({ data, loading, error, selectedDate }) => {
const ChartTable = ({ data, loading, error, selectedDate, selectedSource, itemCount, onItemCountChange }) => {
if (loading) {
return (
<div className="chart-table">
@ -56,6 +57,17 @@ const ChartTable = ({ data, loading, error, selectedDate }) => {
</tbody>
</table>
</div>
{/* JSON Viewer for debugging and data inspection */}
<JsonViewer
data={data}
title={`Raw JSON Data for ${selectedDate || 'Current'} Chart`}
filename={`chart-data-${selectedDate || 'current'}-raw.json`}
selectedDate={selectedDate}
selectedSource={selectedSource}
itemCount={itemCount}
onItemCountChange={onItemCountChange}
/>
</div>
);
};

View File

@ -1,9 +1,23 @@
import React from 'react';
const DownloadButton = ({ data, selectedDate }) => {
const DownloadButton = ({ data, selectedDate, selectedSource, itemCount, onItemCountChange }) => {
const downloadJSON = () => {
if (!data || data.length === 0) return;
// Limit data based on item count for Kworb source
let dataToDownload = data;
if (selectedSource === 'kworb' && itemCount) {
dataToDownload = data.slice(0, itemCount);
}
// For Kworb source, renumber positions sequentially to handle missing numbers
if (selectedSource === 'kworb') {
dataToDownload = dataToDownload.map((item, index) => ({
...item,
order: index + 1 // Sequential numbering starting from 1
}));
}
// Format the date for the title
const formatDate = (dateString) => {
const date = new Date(dateString);
@ -18,8 +32,8 @@ const DownloadButton = ({ data, selectedDate }) => {
const jsonData = {
title: `${formatDate(selectedDate)} - Top Songs`,
date: selectedDate,
totalSongs: data.length,
songs: data
totalSongs: dataToDownload.length,
songs: dataToDownload
};
const jsonContent = JSON.stringify(jsonData, null, 2);

View File

@ -0,0 +1,30 @@
import React from 'react';
const ItemCountSelector = ({ itemCount, onItemCountChange, selectedSource }) => {
// Only show for Kworb source since it has random numbers
if (selectedSource !== 'kworb') {
return null;
}
const itemCountOptions = [40, 50, 60, 70, 80, 90, 100];
return (
<div className="item-count-selector">
<label htmlFor="item-count">Number of items to download:</label>
<select
id="item-count"
value={itemCount}
onChange={(e) => onItemCountChange(parseInt(e.target.value))}
className="item-count-select"
>
{itemCountOptions.map(count => (
<option key={count} value={count}>
{count} items
</option>
))}
</select>
</div>
);
};
export default ItemCountSelector;

View File

@ -1,7 +1,9 @@
import React, { useState } from 'react';
import ItemCountSelector from './ItemCountSelector';
const JsonViewer = ({ data, title, filename, year }) => {
const JsonViewer = ({ data, title, filename, year, selectedSource, itemCount, onItemCountChange }) => {
const [isVisible, setIsVisible] = useState(false);
const [customTitle, setCustomTitle] = useState(title || `${year} - Top 50 Songs`);
const [selectedProperties, setSelectedProperties] = useState({
position: true,
title: true,
@ -15,12 +17,23 @@ const JsonViewer = ({ data, title, filename, year }) => {
const formatData = () => {
if (!data || data.length === 0) return null;
const filteredSongs = data.map(item => {
// Limit data based on item count for Kworb source
let dataToProcess = data;
if (selectedSource === 'kworb' && itemCount) {
dataToProcess = data.slice(0, itemCount);
}
const filteredSongs = dataToProcess.map((item, index) => {
const filteredSong = {};
// Map order to position to match the expected format
if (selectedProperties.position && item.hasOwnProperty('order')) {
filteredSong.position = item.order;
// For Kworb source, renumber positions sequentially to handle missing numbers
if (selectedSource === 'kworb') {
filteredSong.position = index + 1; // Sequential numbering starting from 1
} else {
filteredSong.position = item.order; // Keep original order for other sources
}
}
// Add other selected properties
@ -34,7 +47,7 @@ const JsonViewer = ({ data, title, filename, year }) => {
});
return {
title: `${year} - Top 50 Songs`,
title: customTitle,
songs: filteredSongs
};
};
@ -73,8 +86,46 @@ const JsonViewer = ({ data, title, filename, year }) => {
document.body.removeChild(link);
};
const updateTitle = () => {
// This function can be used to update the title if needed
// For now, the title updates automatically as the user types
};
return (
<div className="json-viewer">
{/* Form Section with Title Input and Item Count Selector */}
<div className="json-form-section">
<div className="form-row">
<div className="form-group">
<label htmlFor="json-title">JSON Title:</label>
<input
id="json-title"
type="text"
value={customTitle}
onChange={(e) => setCustomTitle(e.target.value)}
className="title-input"
placeholder="Enter custom title for JSON"
/>
</div>
{/* Item Count Selector - only for Kworb */}
<ItemCountSelector
itemCount={itemCount}
onItemCountChange={onItemCountChange}
selectedSource={selectedSource}
/>
</div>
<div className="form-actions">
<button
onClick={updateTitle}
className="update-btn"
>
Update Title
</button>
</div>
</div>
<button
onClick={() => setIsVisible(!isVisible)}
className="json-toggle-btn"
@ -85,7 +136,7 @@ const JsonViewer = ({ data, title, filename, year }) => {
{isVisible && (
<div className="json-container">
<div className="json-header">
<h4>{title}</h4>
<h4>{customTitle}</h4>
<div className="json-actions">
<button onClick={copyToClipboard} className="copy-btn">
Copy JSON

View File

@ -1,12 +1,26 @@
import React from 'react';
const YearlyDownloadButton = ({ data, year }) => {
const YearlyDownloadButton = ({ data, year, selectedSource, itemCount, onItemCountChange }) => {
const downloadJSON = () => {
if (!data || data.length === 0) return;
// Limit data based on item count for Kworb source
let dataToDownload = data;
if (selectedSource === 'kworb' && itemCount) {
dataToDownload = data.slice(0, itemCount);
}
// For Kworb source, renumber positions sequentially to handle missing numbers
if (selectedSource === 'kworb') {
dataToDownload = dataToDownload.map((item, index) => ({
...item,
order: index + 1 // Sequential numbering starting from 1
}));
}
// Create JSON content for yearly top songs data
// Filter out calculation properties to keep original download format
const songsForDownload = data.map(song => ({
const songsForDownload = dataToDownload.map(song => ({
order: song.order,
title: song.title,
artist: song.artist

View File

@ -1,7 +1,7 @@
import React from 'react';
import JsonViewer from './JsonViewer';
const YearlyTopSongs = ({ data, loading, error, year }) => {
const YearlyTopSongs = ({ data, loading, error, year, selectedSource, itemCount, onItemCountChange }) => {
if (loading) {
return (
<div className="chart-table">
@ -75,6 +75,9 @@ const YearlyTopSongs = ({ data, loading, error, year }) => {
title={`Raw JSON Data for ${year} Yearly Top Songs`}
filename={`yearly-top-songs-${year}-raw.json`}
year={year}
selectedSource={selectedSource}
itemCount={itemCount}
onItemCountChange={onItemCountChange}
/>
</div>
);

View File

@ -101,23 +101,22 @@ body {
.source-name {
font-weight: 600;
font-size: 14px;
font-size: 1rem;
margin-bottom: 4px;
}
.source-description {
font-size: 12px;
font-size: 0.8rem;
opacity: 0.8;
text-align: center;
}
.chart-type-selector {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
padding: 15px;
background: white;
border-radius: 6px;
border-radius: 8px;
border: 1px solid #e9ecef;
}
@ -129,58 +128,55 @@ body {
.chart-type-selector select {
padding: 8px 12px;
border: 1px solid #e9ecef;
border-radius: 4px;
font-size: 14px;
border: 1px solid #ced4da;
border-radius: 6px;
background: white;
min-width: 150px;
font-size: 0.9rem;
color: #495057;
cursor: pointer;
transition: border-color 0.2s ease;
}
.chart-type-selector select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
/* Shazam info message */
.shazam-info {
display: flex;
align-items: center;
padding: 15px;
background: #e3f2fd;
border: 1px solid #2196f3;
border-radius: 6px;
margin-bottom: 10px;
border: 1px solid #bbdefb;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
}
.info-message {
text-align: center;
width: 100%;
}
.info-message strong {
color: #1976d2;
font-size: 16px;
display: block;
color: #1976d2;
font-size: 1.1rem;
margin-bottom: 5px;
}
.info-message p {
color: #424242;
margin: 0;
font-size: 14px;
font-size: 0.9rem;
}
/* Year selector and view mode */
.year-selector-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
align-items: start;
display: flex;
flex-direction: column;
gap: 15px;
}
.year-selector {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
@ -190,24 +186,26 @@ body {
}
.year-select {
padding: 12px;
border: 2px solid #e9ecef;
padding: 10px 15px;
border: 1px solid #ced4da;
border-radius: 6px;
font-size: 16px;
background: white;
font-size: 1rem;
color: #495057;
cursor: pointer;
transition: border-color 0.2s ease;
}
.year-select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
/* View mode selector */
.view-mode-selector {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
gap: 15px;
}
.view-mode-selector label {
@ -221,104 +219,106 @@ body {
}
.view-mode-btn {
flex: 1;
padding: 10px 16px;
border: 2px solid #e9ecef;
border-radius: 6px;
padding: 10px 20px;
border: 1px solid #ced4da;
background: white;
color: #495057;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.9rem;
font-weight: 500;
}
.view-mode-btn:hover {
border-color: #667eea;
color: #667eea;
background: #f8f9fa;
border-color: #adb5bd;
}
.view-mode-btn.active {
background: #667eea;
border-color: #667eea;
color: white;
border-color: #667eea;
}
/* Date list */
.date-list h3 {
margin-bottom: 15px;
color: #495057;
font-size: 18px;
font-size: 1.2rem;
}
.date-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
margin-bottom: 20px;
max-height: 300px;
overflow-y: auto;
}
.date-item {
padding: 10px;
padding: 12px;
background: white;
border: 2px solid #e9ecef;
border: 1px solid #e9ecef;
border-radius: 6px;
text-align: center;
cursor: pointer;
text-align: center;
transition: all 0.2s ease;
font-size: 14px;
font-size: 0.9rem;
}
.date-item:hover {
background: #f8f9fa;
border-color: #667eea;
color: #667eea;
}
.date-item.selected {
background: #667eea;
border-color: #667eea;
color: white;
border-color: #667eea;
}
/* Chart section */
.chart-section {
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
gap: 20px;
}
.chart-table h3 {
background: #f8f9fa;
padding: 20px;
margin: 0;
border-bottom: 1px solid #dee2e6;
margin-bottom: 15px;
color: #495057;
font-size: 1.2rem;
display: flex;
align-items: center;
gap: 10px;
}
.table-container {
overflow-x: auto;
padding: 20px;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.data-table th,
.data-table td {
padding: 12px;
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #dee2e6;
border-bottom: 1px solid #e9ecef;
}
.data-table th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
position: sticky;
top: 0;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.data-table tr:hover {
@ -326,18 +326,18 @@ body {
}
.rank-cell {
font-weight: 700;
color: #333;
font-weight: 600;
color: #495057;
width: 60px;
}
.title-cell {
font-weight: 600;
color: #333;
font-weight: 500;
color: #212529;
}
.artist-cell {
color: #666;
color: #6c757d;
font-style: italic;
}
@ -354,140 +354,300 @@ body {
}
.appearances-cell {
font-weight: 500;
font-weight: 600;
color: #6f42c1;
text-align: center;
}
.data-table tr:nth-child(1) .rank-cell {
color: #ffd700;
font-weight: 700;
}
.data-table tr:nth-child(2) .rank-cell {
color: #c0c0c0;
font-weight: 700;
}
.data-table tr:nth-child(3) .rank-cell {
color: #cd7f32;
font-weight: 700;
}
/* Download section */
.download-section {
margin-top: 20px;
text-align: center;
display: flex;
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
.download-button {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
padding: 12px 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.2s ease;
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.download-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.download-button:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* Loading and error states */
/* Item count selector */
.item-count-selector {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
margin-bottom: 15px;
}
.item-count-selector label {
font-weight: 600;
color: #495057;
font-size: 0.9rem;
white-space: nowrap;
}
.item-count-select {
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 6px;
background: white;
font-size: 0.9rem;
color: #495057;
cursor: pointer;
transition: border-color 0.2s ease;
min-width: 120px;
}
.item-count-select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
/* JSON viewer specific item count selector */
.json-viewer .item-count-selector {
margin: 0;
background: transparent;
border: none;
padding: 0;
min-width: 180px;
flex-shrink: 0;
}
/* JSON Form Section */
.json-form-section {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
}
.form-row {
display: flex;
gap: 15px;
align-items: flex-end;
margin-bottom: 15px;
flex-wrap: wrap;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 200px;
flex: 1;
}
.form-group label {
font-weight: 600;
color: #495057;
font-size: 0.9rem;
margin-bottom: 4px;
}
.title-input {
padding: 10px 12px;
border: 1px solid #ced4da;
border-radius: 6px;
font-size: 0.9rem;
color: #495057;
background: white;
transition: border-color 0.2s ease;
width: 100%;
box-sizing: border-box;
}
.title-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.form-actions {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.update-btn {
padding: 8px 16px;
background: #28a745;
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.update-btn:hover {
background: #218838;
}
/* JSON viewer specific item count selector */
.json-viewer .item-count-selector label {
font-weight: 600;
color: #495057;
font-size: 0.9rem;
margin-bottom: 4px;
}
.json-viewer .item-count-select {
padding: 10px 12px;
border: 1px solid #ced4da;
border-radius: 6px;
background: white;
font-size: 0.9rem;
color: #495057;
cursor: pointer;
transition: border-color 0.2s ease;
width: 100%;
box-sizing: border-box;
}
.json-viewer .item-count-select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
/* Responsive design for form section */
@media (max-width: 768px) {
.form-row {
flex-direction: column;
gap: 15px;
}
.form-group {
width: 100%;
min-width: auto;
}
.json-viewer .item-count-selector {
width: 100%;
min-width: auto;
}
.form-actions {
justify-content: center;
}
}
.loading {
text-align: center;
padding: 20px;
padding: 40px;
color: #6c757d;
font-style: italic;
font-size: 1.1rem;
}
.error {
text-align: center;
padding: 20px;
padding: 40px;
color: #dc3545;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 6px;
margin: 10px 0;
border-radius: 8px;
margin: 20px 0;
}
.no-data,
.no-dates {
text-align: center;
padding: 20px;
padding: 40px;
color: #6c757d;
font-style: italic;
background: #f8f9fa;
border-radius: 6px;
}
/* JSON Viewer styles */
.json-viewer {
margin-top: 20px;
}
.json-toggle-btn {
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
padding: 10px 20px;
background: #6c757d;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 10px;
font-size: 0.9rem;
font-weight: 500;
transition: background-color 0.2s ease;
margin-bottom: 15px;
}
.json-toggle-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
background: #5a6268;
}
.json-container {
background: #f8f9fa;
border: 1px solid #dee2e6;
border: 1px solid #e9ecef;
border-radius: 8px;
overflow: hidden;
margin-top: 10px;
padding: 20px;
margin-top: 15px;
}
.json-header {
background: #e9ecef;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #dee2e6;
}
.json-header h4 {
margin: 0;
color: #495057;
font-size: 16px;
font-size: 1.1rem;
}
.property-selector {
background: #f1f3f4;
padding: 15px 20px;
border-bottom: 1px solid #dee2e6;
margin-bottom: 20px;
}
.property-selector h5 {
margin: 0 0 10px 0;
margin-bottom: 10px;
color: #495057;
font-size: 14px;
font-size: 0.9rem;
font-weight: 600;
}
@ -500,67 +660,71 @@ body {
.property-checkbox {
display: flex;
align-items: center;
gap: 6px;
gap: 8px;
padding: 8px 12px;
background: white;
border: 1px solid #dee2e6;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
color: #495057;
transition: all 0.2s ease;
}
.property-checkbox input[type="checkbox"] {
width: 16px;
height: 16px;
margin: 0;
cursor: pointer;
}
.property-label {
font-weight: 500;
text-transform: capitalize;
font-size: 0.9rem;
color: #495057;
cursor: pointer;
}
.json-actions {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.copy-btn, .download-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
font-weight: 500;
transition: all 0.2s ease;
}
.copy-btn {
background: #007bff;
color: white;
}
.copy-btn:hover {
background: #0056b3;
}
.download-btn {
background: #28a745;
color: white;
}
.copy-btn:hover {
background: #218838;
}
.download-btn {
background: #007bff;
color: white;
}
.download-btn:hover {
background: #1e7e34;
background: #0056b3;
}
.json-content {
background: #2d3748;
color: #e2e8f0;
padding: 20px;
margin: 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
background: white;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 0.8rem;
line-height: 1.5;
overflow-x: auto;
color: #495057;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-word;
max-height: 400px;
overflow-y: auto;
}
@ -584,7 +748,7 @@ body {
}
.controls-section {
gap: 15px;
padding: 15px;
}
.source-tabs {
@ -594,11 +758,10 @@ body {
.source-tab {
min-width: auto;
border-radius: 6px;
}
.year-selector-container {
grid-template-columns: 1fr;
gap: 10px;
}
.date-grid {
@ -607,7 +770,21 @@ body {
.data-table th,
.data-table td {
padding: 8px;
font-size: 12px;
padding: 8px 10px;
font-size: 0.8rem;
}
.item-count-selector {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.item-count-selector label {
margin-bottom: 5px;
}
.item-count-select {
width: 100%;
}
}