Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>
This commit is contained in:
parent
bcfa16aeed
commit
ca36e2dbf5
67
backend/debug-html.js
Normal file
67
backend/debug-html.js
Normal file
@ -0,0 +1,67 @@
|
||||
const axios = require('axios');
|
||||
const cheerio = require('cheerio');
|
||||
|
||||
async function debugHTML() {
|
||||
try {
|
||||
console.log('Fetching Shazam Country Chart HTML...');
|
||||
|
||||
const response = await axios.get('https://www.shazam.com/charts/genre/united-states/country', {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
},
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const html = response.data;
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
console.log('HTML length:', html.length);
|
||||
|
||||
// Look for list elements
|
||||
const ulElements = $('ul');
|
||||
console.log('Found', ulElements.length, 'ul elements');
|
||||
|
||||
ulElements.each((i, ul) => {
|
||||
const $ul = $(ul);
|
||||
const liElements = $ul.find('li');
|
||||
console.log(`UL ${i + 1}: Found ${liElements.length} li elements`);
|
||||
|
||||
if (liElements.length > 0) {
|
||||
liElements.each((j, li) => {
|
||||
const $li = $(li);
|
||||
const text = $li.text().trim();
|
||||
if (text.length > 0) {
|
||||
console.log(` LI ${j + 1}: "${text}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Look for elements with numbers
|
||||
console.log('\nLooking for elements with numbers...');
|
||||
$('*').each((i, el) => {
|
||||
const $el = $(el);
|
||||
const text = $el.text().trim();
|
||||
|
||||
// Look for text that starts with a number and contains song-like content
|
||||
if (text.match(/^\d+/) && text.length > 10 && text.length < 200) {
|
||||
console.log(`Element ${i}: "${text}"`);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
debugHTML();
|
||||
14
backend/debug-shazam.js
Normal file
14
backend/debug-shazam.js
Normal file
@ -0,0 +1,14 @@
|
||||
const ShazamService = require('./src/models/shazamService');
|
||||
|
||||
async function debugShazam() {
|
||||
try {
|
||||
console.log('Testing Shazam Country Chart...');
|
||||
const data = await ShazamService.getChartData('country');
|
||||
console.log('Found', data.length, 'songs:');
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
debugShazam();
|
||||
@ -1,14 +1,23 @@
|
||||
const ChartService = require('../models/chartService');
|
||||
const KworbService = require('../models/kworbService');
|
||||
const ShazamService = require('../models/shazamService');
|
||||
|
||||
// Get available dates for a specific year
|
||||
const getChartDates = async (req, res) => {
|
||||
try {
|
||||
const { year } = req.params;
|
||||
const source = req.query.source || 'musicchartsarchive';
|
||||
let dates;
|
||||
|
||||
// Validate year parameter
|
||||
const validatedYear = ChartService.validateYear(year);
|
||||
|
||||
const dates = await ChartService.getAvailableDates(validatedYear);
|
||||
if (source === 'kworb') {
|
||||
dates = await KworbService.getAvailableDates(year);
|
||||
} else if (source === 'shazam') {
|
||||
dates = await ShazamService.getAvailableDates(year);
|
||||
} else {
|
||||
// Validate year parameter
|
||||
const validatedYear = ChartService.validateYear(year);
|
||||
dates = await ChartService.getAvailableDates(validatedYear);
|
||||
}
|
||||
res.json(dates);
|
||||
} catch (error) {
|
||||
console.error('Error in getChartDates:', error);
|
||||
@ -23,6 +32,8 @@ const getChartDates = async (req, res) => {
|
||||
const getChartData = async (req, res) => {
|
||||
try {
|
||||
const { date } = req.params;
|
||||
const source = req.query.source || 'musicchartsarchive';
|
||||
const chartType = req.query.chartType || 'top-200'; // For Shazam
|
||||
|
||||
if (!date) {
|
||||
return res.status(400).json({
|
||||
@ -30,11 +41,17 @@ const getChartData = async (req, res) => {
|
||||
message: 'Date parameter is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Validate date format
|
||||
ChartService.validateDate(date);
|
||||
|
||||
const data = await ChartService.getChartData(date);
|
||||
|
||||
let data;
|
||||
if (source === 'kworb') {
|
||||
data = await KworbService.getChartData(date);
|
||||
} else if (source === 'shazam') {
|
||||
data = await ShazamService.getChartData(chartType);
|
||||
} else {
|
||||
// Validate date format
|
||||
ChartService.validateDate(date);
|
||||
data = await ChartService.getChartData(date);
|
||||
}
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
console.error('Error in getChartData:', error);
|
||||
@ -49,11 +66,19 @@ const getChartData = async (req, res) => {
|
||||
const getYearlyTopSongs = async (req, res) => {
|
||||
try {
|
||||
const { year } = req.params;
|
||||
const source = req.query.source || 'musicchartsarchive';
|
||||
const chartType = req.query.chartType || 'top-200'; // For Shazam
|
||||
|
||||
// Validate year parameter
|
||||
const validatedYear = ChartService.validateYear(year);
|
||||
|
||||
const yearlySongs = await ChartService.getYearlyTopSongs(validatedYear);
|
||||
let yearlySongs;
|
||||
if (source === 'kworb') {
|
||||
yearlySongs = await KworbService.getYearlyTopSongs(year);
|
||||
} else if (source === 'shazam') {
|
||||
yearlySongs = await ShazamService.getYearlyTopSongs(year, chartType);
|
||||
} else {
|
||||
// Validate year parameter
|
||||
const validatedYear = ChartService.validateYear(year);
|
||||
yearlySongs = await ChartService.getYearlyTopSongs(validatedYear);
|
||||
}
|
||||
res.json(yearlySongs);
|
||||
} catch (error) {
|
||||
console.error('Error in getYearlyTopSongs:', error);
|
||||
@ -64,8 +89,23 @@ const getYearlyTopSongs = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Get available Shazam chart types
|
||||
const getShazamChartTypes = async (req, res) => {
|
||||
try {
|
||||
const chartTypes = ShazamService.getAvailableCharts();
|
||||
res.json(chartTypes);
|
||||
} catch (error) {
|
||||
console.error('Error in getShazamChartTypes:', error);
|
||||
res.status(400).json({
|
||||
error: 'Failed to fetch Shazam chart types',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getChartDates,
|
||||
getChartData,
|
||||
getYearlyTopSongs
|
||||
getYearlyTopSongs,
|
||||
getShazamChartTypes
|
||||
};
|
||||
165
backend/src/models/kworbService.js
Normal file
165
backend/src/models/kworbService.js
Normal file
@ -0,0 +1,165 @@
|
||||
const axios = require('axios');
|
||||
const cheerio = require('cheerio');
|
||||
|
||||
const BASE_URL = 'https://kworb.net/apple_songs/';
|
||||
|
||||
class KworbService {
|
||||
// Get available dates from the archive (2022 and forward)
|
||||
static async getAvailableDates(year) {
|
||||
if (parseInt(year) < 2022) return [];
|
||||
const archiveUrl = BASE_URL + 'archive/';
|
||||
console.log('[kworb] Fetching archive:', archiveUrl);
|
||||
const res = await axios.get(archiveUrl);
|
||||
const $ = cheerio.load(res.data);
|
||||
const dates = [];
|
||||
$('a').each((i, el) => {
|
||||
const href = $(el).attr('href');
|
||||
// kworb archive links are in the format YYYYMMDD.html
|
||||
const match = href && href.match(/(\d{8})\.html/);
|
||||
if (match) {
|
||||
// Convert to YYYY-MM-DD for API
|
||||
const date = match[1].replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
|
||||
const y = parseInt(date.substring(0, 4));
|
||||
if (y >= 2022 && date.startsWith(year)) {
|
||||
dates.push({
|
||||
date,
|
||||
formattedDate: date // You can format as needed
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
// Optionally add 'today' as the current chart if year is current year
|
||||
const now = new Date();
|
||||
if (parseInt(year) === now.getFullYear()) {
|
||||
dates.unshift({
|
||||
date: 'today',
|
||||
formattedDate: 'Today'
|
||||
});
|
||||
}
|
||||
// Sort dates ascending
|
||||
dates.sort((a, b) => a.date.localeCompare(b.date));
|
||||
console.log('[kworb] Available dates for', year, ':', dates.map(d => d.date));
|
||||
return dates;
|
||||
}
|
||||
|
||||
// Get chart data for a specific date
|
||||
static async getChartData(date) {
|
||||
let url;
|
||||
if (date === 'today') {
|
||||
url = BASE_URL + 'index.html';
|
||||
} else {
|
||||
// kworb archive URLs use YYYYMMDD (no dashes)
|
||||
const dateNoDash = date.replace(/-/g, '');
|
||||
url = BASE_URL + 'archive/' + dateNoDash + '.html';
|
||||
}
|
||||
console.log('[kworb] Fetching chart data:', url);
|
||||
const res = await axios.get(url);
|
||||
const $ = cheerio.load(res.data);
|
||||
let songs = [];
|
||||
$('table.sortable tbody tr').each((i, el) => {
|
||||
const tds = $(el).find('td');
|
||||
if (tds.length >= 13) {
|
||||
const us = parseInt($(tds[9]).text().trim());
|
||||
if (isNaN(us)) return; // Only include songs with a valid US position
|
||||
const artistAndTitle = $(tds[2]).text().trim();
|
||||
// Split "Artist - Title"
|
||||
const [artist, ...titleParts] = artistAndTitle.split(' - ');
|
||||
const title = titleParts.join(' - ');
|
||||
if (artist && title) {
|
||||
songs.push({
|
||||
order: us, // Use US chart position as the order
|
||||
artist: artist.trim(),
|
||||
title: title.trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
// Sort by US ranking (ascending) and take top 50
|
||||
songs = songs.sort((a, b) => a.order - b.order).slice(0, 50);
|
||||
console.log('[kworb] Found', songs.length, 'songs with US ranking');
|
||||
return songs;
|
||||
}
|
||||
|
||||
// Get yearly top songs for a year (sample charts throughout the year, aggregate by best US position)
|
||||
static async getYearlyTopSongs(year) {
|
||||
const dates = await this.getAvailableDates(year);
|
||||
if (dates.length === 0) return [];
|
||||
|
||||
// Sample charts more frequently - every 3-4 days instead of weekly
|
||||
const sampledDates = [];
|
||||
const step = Math.max(1, Math.floor(dates.length / 52)); // Aim for ~52 samples per year
|
||||
|
||||
for (let i = 0; i < dates.length; i += step) {
|
||||
sampledDates.push(dates[i].date);
|
||||
}
|
||||
|
||||
// Also include the last date if not already included
|
||||
if (sampledDates.length > 0 && sampledDates[sampledDates.length - 1] !== dates[dates.length - 1].date) {
|
||||
sampledDates.push(dates[dates.length - 1].date);
|
||||
}
|
||||
|
||||
console.log('[kworb] Sampled dates for yearly top:', sampledDates.length, 'charts');
|
||||
console.log('[kworb] Sample dates:', sampledDates.slice(0, 10), '...');
|
||||
// Aggregate songs by title+artist, track best US position and appearances
|
||||
const songMap = {};
|
||||
let totalSongsProcessed = 0;
|
||||
|
||||
for (const date of sampledDates) {
|
||||
try {
|
||||
const chart = await this.getChartData(date);
|
||||
totalSongsProcessed += chart.length;
|
||||
|
||||
chart.forEach(song => {
|
||||
if (!song.title || !song.artist) return;
|
||||
const key = song.title + '|' + song.artist;
|
||||
if (!songMap[key]) {
|
||||
songMap[key] = {
|
||||
title: song.title,
|
||||
artist: song.artist,
|
||||
bestUS: song.order,
|
||||
appearances: 0
|
||||
};
|
||||
}
|
||||
if (song.order < songMap[key].bestUS) {
|
||||
songMap[key].bestUS = song.order;
|
||||
}
|
||||
songMap[key].appearances++;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('[kworb] Error processing date', date, ':', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[kworb] Processed', totalSongsProcessed, 'total songs across', sampledDates.length, 'charts');
|
||||
console.log('[kworb] Found', Object.keys(songMap).length, 'unique songs');
|
||||
// Sort by best US position (ascending), then appearances (descending), then title for consistency
|
||||
let topSongs = Object.values(songMap)
|
||||
.sort((a, b) => {
|
||||
if (a.bestUS !== b.bestUS) return a.bestUS - b.bestUS;
|
||||
if (a.appearances !== b.appearances) return b.appearances - a.appearances;
|
||||
return a.title.localeCompare(b.title); // Consistent tie-breaker
|
||||
})
|
||||
.slice(0, 50)
|
||||
.map((song, index) => ({
|
||||
order: index + 1, // Create new ranking based on aggregated performance
|
||||
title: song.title,
|
||||
artist: song.artist,
|
||||
appearances: song.appearances,
|
||||
bestUS: song.bestUS // Keep the best US position for reference
|
||||
}));
|
||||
console.log('[kworb] Yearly top songs:', topSongs.length, 'songs');
|
||||
console.log('[kworb] Sample of yearly data:', topSongs.slice(0, 5).map(s => `${s.order}. ${s.title} - ${s.artist} (appearances: ${s.appearances}, best US: ${s.bestUS})`));
|
||||
return topSongs;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: get ISO week number from date string (YYYY-MM-DD)
|
||||
function getWeekOfYear(dateStr) {
|
||||
const d = new Date(dateStr);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
d.setDate(d.getDate() + 4 - (d.getDay() || 7));
|
||||
const yearStart = new Date(d.getFullYear(), 0, 1);
|
||||
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
|
||||
}
|
||||
|
||||
module.exports = KworbService;
|
||||
694
backend/src/models/shazamService.js
Normal file
694
backend/src/models/shazamService.js
Normal file
@ -0,0 +1,694 @@
|
||||
const axios = require('axios');
|
||||
const cheerio = require('cheerio');
|
||||
|
||||
const BASE_URL = 'https://www.shazam.com/charts';
|
||||
|
||||
class ShazamService {
|
||||
// Get available chart types
|
||||
static getAvailableCharts() {
|
||||
return [
|
||||
{ id: 'top-200', name: 'Top 200', url: `${BASE_URL}/top-200/united-states` },
|
||||
{ id: 'pop', name: 'Pop', url: `${BASE_URL}/genre/united-states/pop` },
|
||||
{ id: 'hip-hop-rap', name: 'Hip-Hop/Rap', url: `${BASE_URL}/genre/united-states/hip-hop-rap` },
|
||||
{ id: 'country', name: 'Country', url: `${BASE_URL}/genre/united-states/country` }
|
||||
];
|
||||
}
|
||||
|
||||
// Get chart data for a specific chart type
|
||||
static async getChartData(chartType = 'top-200') {
|
||||
const charts = this.getAvailableCharts();
|
||||
const chart = charts.find(c => c.id === chartType);
|
||||
|
||||
if (!chart) {
|
||||
throw new Error(`Invalid chart type: ${chartType}. Available: ${charts.map(c => c.id).join(', ')}`);
|
||||
}
|
||||
|
||||
console.log('[shazam] Fetching chart data from:', chart.url);
|
||||
return await this.scrapeHTMLData(chart.url);
|
||||
}
|
||||
|
||||
// Scrape HTML data from Shazam
|
||||
static async scrapeHTMLData(url) {
|
||||
console.log('[shazam] Scraping HTML from:', url);
|
||||
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
},
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const html = response.data;
|
||||
return this.parseHTML(html);
|
||||
}
|
||||
|
||||
// Parse HTML data based on Shazam's structure
|
||||
static parseHTML(html) {
|
||||
const songs = [];
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
console.log('[shazam] Parsing HTML content...');
|
||||
console.log('[shazam] HTML length:', html.length);
|
||||
|
||||
// Save raw HTML for debugging (first 5000 chars)
|
||||
console.log('[shazam] Raw HTML preview:', html.substring(0, 5000));
|
||||
|
||||
// First, try the new Shazam-specific parsing method
|
||||
console.log('[shazam] Trying Shazam-specific structure parsing...');
|
||||
this.extractSongsFromShazamStructure($, songs);
|
||||
|
||||
// Also try the exact structure from the provided HTML
|
||||
console.log('[shazam] Trying exact HTML structure parsing...');
|
||||
this.extractSongsFromExactStructure($, songs);
|
||||
|
||||
// If no songs found, try the existing methods
|
||||
if (songs.length === 0) {
|
||||
console.log('[shazam] No songs found with Shazam structure, trying alternative methods...');
|
||||
|
||||
// Look for the specific chart list container class
|
||||
const chartContainer = $('.ListShowMoreLess_container__t4TNB.page_chartList__aBclW');
|
||||
console.log('[shazam] Chart container found:', chartContainer.length);
|
||||
|
||||
if (chartContainer.length === 0) {
|
||||
console.log('[shazam] Chart container not found, trying alternative selectors...');
|
||||
|
||||
// Try alternative selectors
|
||||
const alternativeSelectors = [
|
||||
'.page_chartList__aBclW',
|
||||
'.ListShowMoreLess_container__t4TNB',
|
||||
'[class*="chartList"]',
|
||||
'[class*="ListShowMoreLess"]',
|
||||
'[class*="chart"]',
|
||||
'[class*="list"]'
|
||||
];
|
||||
|
||||
for (const selector of alternativeSelectors) {
|
||||
const container = $(selector);
|
||||
console.log('[shazam] Selector', selector, 'found:', container.length, 'elements');
|
||||
if (container.length > 0) {
|
||||
console.log('[shazam] Found container with selector:', selector);
|
||||
this.extractSongsFromContainer(container, songs);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('[shazam] Found chart container with specific class');
|
||||
this.extractSongsFromContainer(chartContainer, songs);
|
||||
}
|
||||
|
||||
// If still no songs, try extracting from image alt text
|
||||
if (songs.length === 0) {
|
||||
console.log('[shazam] No songs found in containers, trying image alt text...');
|
||||
this.extractSongsFromImages($, songs);
|
||||
}
|
||||
|
||||
// If still no songs, try extracting from JSON-LD structured data
|
||||
if (songs.length === 0) {
|
||||
console.log('[shazam] No songs found in images, trying JSON-LD data...');
|
||||
this.extractSongsFromJSONLD($, songs);
|
||||
}
|
||||
|
||||
// If still no songs, try extracting from list elements (ul/li)
|
||||
if (songs.length === 0) {
|
||||
console.log('[shazam] No songs found in JSON-LD, trying list elements...');
|
||||
this.extractSongsFromListElements($, songs);
|
||||
}
|
||||
|
||||
// If still no songs, try looking for Shazam-specific elements
|
||||
if (songs.length === 0) {
|
||||
console.log('[shazam] No songs found in list elements, trying Shazam-specific elements...');
|
||||
this.extractSongsFromShazamElements($, songs);
|
||||
}
|
||||
|
||||
// If still no songs, try a more generic approach
|
||||
if (songs.length === 0) {
|
||||
console.log('[shazam] No songs found in Shazam elements, trying generic approach...');
|
||||
this.extractSongsGeneric($, songs);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by order and take top 50
|
||||
songs.sort((a, b) => a.order - b.order);
|
||||
const topSongs = songs.slice(0, 50);
|
||||
|
||||
console.log('[shazam] Final result:', topSongs.length, 'songs');
|
||||
|
||||
// Log some examples for debugging
|
||||
if (topSongs.length > 0) {
|
||||
console.log('[shazam] Sample songs:', topSongs.slice(0, 3));
|
||||
} else {
|
||||
console.log('[shazam] No songs found. Checking for any numbers in text...');
|
||||
const textContent = $.text();
|
||||
const numberMatches = textContent.match(/\d+/g);
|
||||
console.log('[shazam] Numbers found in text:', numberMatches ? numberMatches.slice(0, 10) : 'none');
|
||||
|
||||
// Look for any text that might contain song data
|
||||
const lines = textContent.split('\n');
|
||||
console.log('[shazam] Text lines (first 20):', lines.slice(0, 20));
|
||||
}
|
||||
|
||||
return topSongs;
|
||||
}
|
||||
|
||||
// Extract songs from a specific container
|
||||
static extractSongsFromContainer(container, songs) {
|
||||
console.log('[shazam] Extracting songs from container...');
|
||||
|
||||
// First, try to find chart items with more specific selectors
|
||||
const chartItems = container.find('[class*="chart-item"], [class*="track-item"], [class*="song-item"], [class*="list-item"]');
|
||||
console.log('[shazam] Found chart items:', chartItems.length);
|
||||
|
||||
if (chartItems.length > 0) {
|
||||
chartItems.each((i, el) => {
|
||||
const $el = cheerio.load(el);
|
||||
const text = $el.text().trim();
|
||||
console.log('[shazam] Chart item text:', text.substring(0, 100));
|
||||
|
||||
// Try different patterns for song data
|
||||
const patterns = [
|
||||
/^(\d+)\.?\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist"
|
||||
/^(\d+)\s+(.+?)\s+[-–]\s+(.+)$/, // "1 Song Title - Artist" (no period)
|
||||
/^(\d+)\.\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist" (with period)
|
||||
/^(\d+)\s*[-–]\s*(.+?)\s*[-–]\s*(.+)$/, // "1 - Song Title - Artist"
|
||||
/^(\d+)\s+(.+?)\s+by\s+(.+)$/i, // "1 Song Title by Artist"
|
||||
/^(\d+)\.\s*(.+?)\s+by\s+(.+)$/i // "1. Song Title by Artist"
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
if (match) {
|
||||
const order = parseInt(match[1]);
|
||||
const title = match[2].trim();
|
||||
const artist = match[3].trim();
|
||||
|
||||
if (!isNaN(order) && title && artist && order <= 200) {
|
||||
// Check if we already have this song
|
||||
const exists = songs.some(s => s.order === order && s.title === title && s.artist === artist);
|
||||
if (!exists) {
|
||||
songs.push({ order, title, artist });
|
||||
console.log('[shazam] Found song:', order, title, '-', artist);
|
||||
}
|
||||
}
|
||||
break; // Found a match, no need to try other patterns
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If no chart items found, try looking for any elements with numbers and text
|
||||
if (songs.length === 0) {
|
||||
console.log('[shazam] No chart items found, trying generic elements...');
|
||||
|
||||
container.find('*').each((i, el) => {
|
||||
const $el = cheerio.load(el);
|
||||
const text = $el.text().trim();
|
||||
|
||||
// Only process elements with substantial text
|
||||
if (text.length > 10 && text.length < 200) {
|
||||
const patterns = [
|
||||
/^(\d+)\.?\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist"
|
||||
/^(\d+)\s+(.+?)\s+(.+)$/, // "1 Song Title Artist" (no dash)
|
||||
/^(\d+)\.\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist" (with period)
|
||||
/^(\d+)\s*[-–]\s*(.+?)\s*[-–]\s*(.+)$/ // "1 - Song Title - Artist"
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
if (match) {
|
||||
const order = parseInt(match[1]);
|
||||
const title = match[2].trim();
|
||||
const artist = match[3].trim();
|
||||
|
||||
if (!isNaN(order) && title && artist && order <= 200) {
|
||||
// Check if we already have this song
|
||||
const exists = songs.some(s => s.order === order && s.title === title && s.artist === artist);
|
||||
if (!exists) {
|
||||
songs.push({ order, title, artist });
|
||||
console.log('[shazam] Found song (generic):', order, title, '-', artist);
|
||||
}
|
||||
}
|
||||
break; // Found a match, no need to try other patterns
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Extract songs from image alt text (Shazam uses this for song info)
|
||||
static extractSongsFromImages($, songs) {
|
||||
let songCount = 0;
|
||||
|
||||
// Look for images with alt text containing song information
|
||||
$('img[alt*="by"]').each((i, el) => {
|
||||
const alt = $(el).attr('alt');
|
||||
if (alt) {
|
||||
// Pattern: "Album artwork for album titled Song Title by Artist"
|
||||
// or "Listen to Song Title by Artist"
|
||||
const patterns = [
|
||||
/album titled (.+?) by (.+)/i,
|
||||
/Listen to (.+?) by (.+)/i,
|
||||
/(.+?) by (.+)/i
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = alt.match(pattern);
|
||||
if (match) {
|
||||
const title = match[1].trim();
|
||||
const artist = match[2].trim();
|
||||
|
||||
// Check if we already have this song
|
||||
const exists = songs.some(s => s.title === title && s.artist === artist);
|
||||
if (!exists) {
|
||||
songCount++;
|
||||
songs.push({ order: songCount, title, artist });
|
||||
console.log('[shazam] Found song from image:', songCount, title, '-', artist);
|
||||
}
|
||||
break; // Found a match, no need to try other patterns
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Extract songs from JSON-LD structured data
|
||||
static extractSongsFromJSONLD($, songs) {
|
||||
try {
|
||||
// Look for JSON-LD script tags
|
||||
$('script[type="application/ld+json"]').each((i, el) => {
|
||||
const scriptContent = $(el).html();
|
||||
if (scriptContent) {
|
||||
try {
|
||||
const jsonData = JSON.parse(scriptContent);
|
||||
console.log('[shazam] Found JSON-LD data:', typeof jsonData);
|
||||
|
||||
// Handle different JSON-LD structures
|
||||
if (jsonData['@graph'] && Array.isArray(jsonData['@graph'])) {
|
||||
// Schema.org MusicPlaylist structure
|
||||
jsonData['@graph'].forEach(item => {
|
||||
if (item['@type'] === 'MusicRecording' && item.name && item.byArtist) {
|
||||
const title = item.name;
|
||||
const artist = typeof item.byArtist === 'object' ? item.byArtist.name : item.byArtist;
|
||||
const position = item.position || songs.length + 1;
|
||||
|
||||
const exists = songs.some(s => s.title === title && s.artist === artist);
|
||||
if (!exists) {
|
||||
songs.push({ order: position, title, artist });
|
||||
console.log('[shazam] Found song from JSON-LD:', position, title, '-', artist);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (jsonData['@type'] === 'MusicPlaylist' && jsonData.track) {
|
||||
// Direct playlist structure
|
||||
jsonData.track.forEach((track, index) => {
|
||||
if (track.name && track.byArtist) {
|
||||
const title = track.name;
|
||||
const artist = typeof track.byArtist === 'object' ? track.byArtist.name : track.byArtist;
|
||||
const position = track.position || index + 1;
|
||||
|
||||
const exists = songs.some(s => s.title === title && s.artist === artist);
|
||||
if (!exists) {
|
||||
songs.push({ order: position, title, artist });
|
||||
console.log('[shazam] Found song from JSON-LD playlist:', position, title, '-', artist);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.log('[shazam] Failed to parse JSON-LD:', parseError.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('[shazam] Error extracting from JSON-LD:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract songs from list elements (ul/li structure)
|
||||
static extractSongsFromListElements($, songs) {
|
||||
console.log('[shazam] Looking for list elements (ul/li)...');
|
||||
|
||||
// Look for all ul elements
|
||||
$('ul').each((ulIndex, ul) => {
|
||||
const $ul = $(ul);
|
||||
const liElements = $ul.find('li');
|
||||
console.log('[shazam] Found ul with', liElements.length, 'li elements');
|
||||
|
||||
if (liElements.length > 0) {
|
||||
liElements.each((liIndex, li) => {
|
||||
const $li = $(li);
|
||||
const text = $li.text().trim();
|
||||
console.log('[shazam] Li text:', text);
|
||||
|
||||
// Try different patterns for song data in list items
|
||||
const patterns = [
|
||||
/^(\d+)\.?\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist"
|
||||
/^(\d+)\s+(.+?)\s+[-–]\s+(.+)$/, // "1 Song Title - Artist" (no period)
|
||||
/^(\d+)\.\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist" (with period)
|
||||
/^(\d+)\s*[-–]\s*(.+?)\s*[-–]\s*(.+)$/, // "1 - Song Title - Artist"
|
||||
/^(\d+)\s+(.+?)\s+by\s+(.+)$/i, // "1 Song Title by Artist"
|
||||
/^(\d+)\.\s*(.+?)\s+by\s+(.+)$/i, // "1. Song Title by Artist"
|
||||
/^(\d+)\s+(.+?)\s*[-–]\s*(.+)$/ // "1 Song Title - Artist" (flexible spacing)
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
if (match) {
|
||||
const order = parseInt(match[1]);
|
||||
const title = match[2].trim();
|
||||
const artist = match[3].trim();
|
||||
|
||||
if (!isNaN(order) && title && artist && order <= 200) {
|
||||
// Check if we already have this song
|
||||
const exists = songs.some(s => s.order === order && s.title === title && s.artist === artist);
|
||||
if (!exists) {
|
||||
songs.push({ order, title, artist });
|
||||
console.log('[shazam] Found song from list:', order, title, '-', artist);
|
||||
}
|
||||
}
|
||||
break; // Found a match, no need to try other patterns
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Extract songs from Shazam-specific elements
|
||||
static extractSongsFromShazamElements($, songs) {
|
||||
console.log('[shazam] Looking for Shazam-specific elements...');
|
||||
|
||||
// Look for elements with Shazam-specific classes or attributes
|
||||
const shazamSelectors = [
|
||||
'[class*="track"]',
|
||||
'[class*="song"]',
|
||||
'[class*="chart-item"]',
|
||||
'[class*="music"]',
|
||||
'[data-testid*="track"]',
|
||||
'[data-testid*="song"]'
|
||||
];
|
||||
|
||||
for (const selector of shazamSelectors) {
|
||||
const elements = $(selector);
|
||||
console.log('[shazam] Selector', selector, 'found:', elements.length, 'elements');
|
||||
|
||||
elements.each((i, el) => {
|
||||
const $el = $(el);
|
||||
const text = $el.text().trim();
|
||||
|
||||
if (text.length > 5) {
|
||||
console.log('[shazam] Shazam element text:', text.substring(0, 100));
|
||||
|
||||
// Try different patterns for song data
|
||||
const patterns = [
|
||||
/^(\d+)\.?\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist"
|
||||
/^(\d+)\s+(.+?)\s+[-–]\s+(.+)$/, // "1 Song Title - Artist" (no period)
|
||||
/^(\d+)\.\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist" (with period)
|
||||
/^(\d+)\s*[-–]\s*(.+?)\s*[-–]\s*(.+)$/, // "1 - Song Title - Artist"
|
||||
/^(\d+)\s+(.+?)\s+by\s+(.+)$/i, // "1 Song Title by Artist"
|
||||
/^(\d+)\.\s*(.+?)\s+by\s+(.+)$/i // "1. Song Title by Artist"
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
if (match) {
|
||||
const order = parseInt(match[1]);
|
||||
const title = match[2].trim();
|
||||
const artist = match[3].trim();
|
||||
|
||||
if (!isNaN(order) && title && artist && order <= 200) {
|
||||
// Check if we already have this song
|
||||
const exists = songs.some(s => s.order === order && s.title === title && s.artist === artist);
|
||||
if (!exists) {
|
||||
songs.push({ order, title, artist });
|
||||
console.log('[shazam] Found song from Shazam element:', order, title, '-', artist);
|
||||
}
|
||||
}
|
||||
break; // Found a match, no need to try other patterns
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Generic song extraction as fallback
|
||||
static extractSongsGeneric($, songs) {
|
||||
const textContent = $.text();
|
||||
const lines = textContent.split('\n');
|
||||
|
||||
console.log('[shazam] Text content length:', textContent.length);
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// Try different patterns for song data
|
||||
const patterns = [
|
||||
/^(\d+)\.?\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist"
|
||||
/^(\d+)\s+(.+?)\s+(.+)$/, // "1 Song Title Artist" (no dash)
|
||||
/^(\d+)\.\s*(.+?)\s*[-–]\s*(.+)$/ // "1. Song Title - Artist" (with period)
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = trimmedLine.match(pattern);
|
||||
if (match) {
|
||||
const order = parseInt(match[1]);
|
||||
const title = match[2].trim();
|
||||
const artist = match[3].trim();
|
||||
|
||||
if (!isNaN(order) && title && artist && order <= 200) {
|
||||
// Check if we already have this song
|
||||
const exists = songs.some(s => s.order === order && s.title === title && s.artist === artist);
|
||||
if (!exists) {
|
||||
songs.push({ order, title, artist });
|
||||
console.log('[shazam] Found song (generic):', order, title, '-', artist);
|
||||
}
|
||||
}
|
||||
break; // Found a match, no need to try other patterns
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract songs from the specific Shazam HTML structure
|
||||
static extractSongsFromShazamStructure($, songs) {
|
||||
console.log('[shazam] Extracting songs from Shazam structure...');
|
||||
|
||||
// Look for the specific ul structure with li elements containing song data
|
||||
$('ul').each((ulIndex, ul) => {
|
||||
const $ul = $(ul);
|
||||
const liElements = $ul.find('li');
|
||||
console.log('[shazam] Found ul with', liElements.length, 'li elements');
|
||||
|
||||
if (liElements.length > 0) {
|
||||
liElements.each((liIndex, li) => {
|
||||
const $li = $(li);
|
||||
|
||||
// Look for the specific Shazam structure based on the provided HTML
|
||||
const rankingNumber = $li.find('.SongItem-module_rankingNumber__3oDWK').text().trim();
|
||||
|
||||
// Find song title - try multiple approaches
|
||||
let songTitle = '';
|
||||
const songTitleLink = $li.find('a[data-test-id="charts_userevent_list_songTitle"]');
|
||||
if (songTitleLink.length > 0) {
|
||||
songTitle = songTitleLink.text().trim() || songTitleLink.attr('aria-label') || '';
|
||||
}
|
||||
|
||||
// If no song title found, try alternative selectors
|
||||
if (!songTitle) {
|
||||
const titleSelectors = [
|
||||
'a[aria-label]',
|
||||
'.SongItem-module_ellipisLink__DsCMc a',
|
||||
'a.common_link__7If7r'
|
||||
];
|
||||
|
||||
for (const selector of titleSelectors) {
|
||||
const titleElement = $li.find(selector);
|
||||
if (titleElement.length > 0) {
|
||||
const ariaLabel = titleElement.attr('aria-label');
|
||||
if (ariaLabel && ariaLabel !== rankingNumber) {
|
||||
songTitle = ariaLabel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find artist information - try multiple approaches
|
||||
let artist = '';
|
||||
|
||||
// Method 1: Look for artist link with specific data-test-id
|
||||
const artistLink = $li.find('a[data-test-id="charts_userevent_list_artistName"]');
|
||||
if (artistLink.length > 0) {
|
||||
artist = artistLink.text().trim() || artistLink.attr('aria-label') || '';
|
||||
}
|
||||
|
||||
// Method 2: Look for artist in the metadata line
|
||||
if (!artist) {
|
||||
const metadataLine = $li.find('.SongItem-module_metadataLine__7Mm6B');
|
||||
if (metadataLine.length > 0) {
|
||||
// Look for any text that's not the song title
|
||||
const metadataText = metadataLine.text().trim();
|
||||
if (metadataText && metadataText !== songTitle) {
|
||||
artist = metadataText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Look for artist in the main items container
|
||||
if (!artist) {
|
||||
const mainContainer = $li.find('.SongItem-module_mainItemsContainer__9MRor');
|
||||
if (mainContainer.length > 0) {
|
||||
// Find all text elements and look for artist
|
||||
const textElements = mainContainer.find('a, span');
|
||||
for (let i = 0; i < textElements.length; i++) {
|
||||
const element = $(textElements[i]);
|
||||
const text = element.text().trim();
|
||||
if (text && text !== songTitle && text !== rankingNumber && !text.match(/^\d+$/)) {
|
||||
artist = text;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 4: Extract from image alt text
|
||||
if (!artist) {
|
||||
const img = $li.find('img');
|
||||
if (img.length > 0) {
|
||||
const altText = img.attr('alt') || '';
|
||||
// Extract artist from alt text like "Listen to Song Title by Artist"
|
||||
const altMatch = altText.match(/by (.+)$/);
|
||||
if (altMatch) {
|
||||
artist = altMatch[1].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 5: Extract from any text content as fallback
|
||||
if (!artist) {
|
||||
const liText = $li.text().trim();
|
||||
// Try to find artist after the song title
|
||||
if (songTitle && liText.includes(songTitle)) {
|
||||
const afterTitle = liText.substring(liText.indexOf(songTitle) + songTitle.length).trim();
|
||||
if (afterTitle) {
|
||||
// Remove common prefixes and clean up
|
||||
artist = afterTitle.replace(/^[-–\s]+/, '').trim();
|
||||
// Remove any remaining numbers or ranking info
|
||||
artist = artist.replace(/\d+$/, '').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[shazam] Parsed song data:', {
|
||||
rankingNumber,
|
||||
songTitle,
|
||||
artist,
|
||||
liText: $li.text().trim().substring(0, 100)
|
||||
});
|
||||
|
||||
// Validate and add the song
|
||||
if (rankingNumber && songTitle && artist) {
|
||||
const order = parseInt(rankingNumber);
|
||||
if (!isNaN(order) && order <= 200) {
|
||||
// Clean up artist name (remove any remaining ranking numbers)
|
||||
artist = artist.replace(/\d+$/, '').trim();
|
||||
|
||||
// Check if we already have this song
|
||||
const exists = songs.some(s => s.order === order && s.title === songTitle && s.artist === artist);
|
||||
if (!exists) {
|
||||
songs.push({ order, title: songTitle, artist });
|
||||
console.log('[shazam] Found song from Shazam structure:', order, songTitle, '-', artist);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[shazam] Extracted', songs.length, 'songs from Shazam structure');
|
||||
}
|
||||
|
||||
// Extract songs from the exact HTML structure provided
|
||||
static extractSongsFromExactStructure($, songs) {
|
||||
console.log('[shazam] Extracting songs from exact HTML structure...');
|
||||
|
||||
// Look for li elements with the specific class structure
|
||||
$('li').each((liIndex, li) => {
|
||||
const $li = $(li);
|
||||
|
||||
// Check if this li contains the song item structure
|
||||
const songItem = $li.find('.page_songItem__lAdHy');
|
||||
if (songItem.length === 0) return;
|
||||
|
||||
// Extract ranking number from the specific class
|
||||
const rankingElement = $li.find('.SongItem-module_rankingNumber__3oDWK');
|
||||
const rankingNumber = rankingElement.text().trim();
|
||||
|
||||
// Extract song title from the aria-label attribute
|
||||
const songTitleElement = $li.find('a[data-test-id="charts_userevent_list_songTitle"]');
|
||||
let songTitle = '';
|
||||
if (songTitleElement.length > 0) {
|
||||
songTitle = songTitleElement.attr('aria-label') || songTitleElement.text().trim();
|
||||
}
|
||||
|
||||
// Extract artist from the artist link
|
||||
const artistElement = $li.find('a[data-test-id="charts_userevent_list_artistName"]');
|
||||
let artist = '';
|
||||
if (artistElement.length > 0) {
|
||||
artist = artistElement.attr('aria-label') || artistElement.text().trim();
|
||||
}
|
||||
|
||||
// If we have all the data, add the song
|
||||
if (rankingNumber && songTitle && artist) {
|
||||
const order = parseInt(rankingNumber);
|
||||
if (!isNaN(order) && order <= 200) {
|
||||
// Check if we already have this song
|
||||
const exists = songs.some(s => s.order === order && s.title === songTitle && s.artist === artist);
|
||||
if (!exists) {
|
||||
songs.push({ order, title: songTitle, artist });
|
||||
console.log('[shazam] Found song from exact structure:', order, songTitle, '-', artist);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[shazam] Extracted', songs.length, 'songs from exact structure');
|
||||
}
|
||||
|
||||
// Get yearly top songs (for now, just return current chart data)
|
||||
static async getYearlyTopSongs(year, chartType = 'top-200') {
|
||||
console.log('[shazam] Getting yearly top songs for', year, 'chart type:', chartType);
|
||||
|
||||
// For now, return current chart data since Shazam doesn't provide historical data
|
||||
const currentData = await this.getChartData(chartType);
|
||||
|
||||
// Add appearances count (all set to 1 for current data)
|
||||
return currentData.map(song => ({
|
||||
...song,
|
||||
appearances: 1
|
||||
}));
|
||||
}
|
||||
|
||||
// Get available chart types for the API
|
||||
static async getAvailableDates(year) {
|
||||
// Shazam doesn't provide historical date lists
|
||||
// Return current date as "today"
|
||||
return [{
|
||||
date: 'today',
|
||||
formattedDate: 'Today'
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ShazamService;
|
||||
@ -12,4 +12,7 @@ router.get('/data/:date', chartController.getChartData);
|
||||
// Get yearly top songs
|
||||
router.get('/yearly-top/:year', chartController.getYearlyTopSongs);
|
||||
|
||||
// Get available Shazam chart types
|
||||
router.get('/shazam/chart-types', chartController.getShazamChartTypes);
|
||||
|
||||
module.exports = router;
|
||||
13
frontend/build/asset-manifest.json
Normal file
13
frontend/build/asset-manifest.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.6452a0d2.css",
|
||||
"main.js": "/static/js/main.fe2339b5.js",
|
||||
"index.html": "/index.html",
|
||||
"main.6452a0d2.css.map": "/static/css/main.6452a0d2.css.map",
|
||||
"main.fe2339b5.js.map": "/static/js/main.fe2339b5.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.6452a0d2.css",
|
||||
"static/js/main.fe2339b5.js"
|
||||
]
|
||||
}
|
||||
1
frontend/build/index.html
Normal file
1
frontend/build/index.html
Normal file
@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Chart Data Visualization Application"/><title>Chart Data Visualization</title><script defer="defer" src="/static/js/main.fe2339b5.js"></script><link href="/static/css/main.6452a0d2.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
2
frontend/build/static/css/main.6452a0d2.css
Normal file
2
frontend/build/static/css/main.6452a0d2.css
Normal file
File diff suppressed because one or more lines are too long
1
frontend/build/static/css/main.6452a0d2.css.map
Normal file
1
frontend/build/static/css/main.6452a0d2.css.map
Normal file
File diff suppressed because one or more lines are too long
3
frontend/build/static/js/main.fe2339b5.js
Normal file
3
frontend/build/static/js/main.fe2339b5.js
Normal file
File diff suppressed because one or more lines are too long
39
frontend/build/static/js/main.fe2339b5.js.LICENSE.txt
Normal file
39
frontend/build/static/js/main.fe2339b5.js.LICENSE.txt
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @license React
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* scheduler.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
1
frontend/build/static/js/main.fe2339b5.js.map
Normal file
1
frontend/build/static/js/main.fe2339b5.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -6,18 +6,23 @@ import ChartTable from './components/ChartTable';
|
||||
import DownloadButton from './components/DownloadButton';
|
||||
import YearlyTopSongs from './components/YearlyTopSongs';
|
||||
import YearlyDownloadButton from './components/YearlyDownloadButton';
|
||||
import SourceSelector from './components/SourceSelector';
|
||||
import { useChartDates } from './hooks/useChartDates';
|
||||
import { useChartData } from './hooks/useChartData';
|
||||
import { useYearlyTopSongs } from './hooks/useYearlyTopSongs';
|
||||
import { useShazamChartTypes } from './hooks/useShazamChartTypes';
|
||||
|
||||
function App() {
|
||||
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
|
||||
const [selectedDate, setSelectedDate] = useState(null);
|
||||
const [viewMode, setViewMode] = useState('weekly'); // 'weekly' or 'yearly'
|
||||
const [selectedSource, setSelectedSource] = useState('musicchartsarchive');
|
||||
const [selectedChartType, setSelectedChartType] = useState('top-200');
|
||||
|
||||
const { dates, loading: datesLoading, error: datesError } = useChartDates(selectedYear);
|
||||
const { chartData, loading: dataLoading, error: dataError } = useChartData(selectedDate);
|
||||
const { yearlySongs, loading: yearlyLoading, error: yearlyError } = useYearlyTopSongs(viewMode === 'yearly' ? selectedYear : null);
|
||||
const { chartTypes: shazamChartTypes, loading: chartTypesLoading } = useShazamChartTypes();
|
||||
const { dates, loading: datesLoading, error: datesError } = useChartDates(selectedSource === 'shazam' ? null : selectedYear, selectedSource);
|
||||
const { chartData, loading: dataLoading, error: dataError } = useChartData(selectedSource === 'shazam' ? 'today' : selectedDate, selectedSource, selectedSource === 'shazam' ? selectedChartType : null);
|
||||
const { yearlySongs, loading: yearlyLoading, error: yearlyError } = useYearlyTopSongs(selectedSource === 'shazam' ? null : (viewMode === 'yearly' ? selectedYear : null), selectedSource, selectedSource === 'shazam' ? selectedChartType : null);
|
||||
|
||||
const handleYearChange = (year) => {
|
||||
setSelectedYear(year);
|
||||
@ -35,6 +40,19 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSourceChange = (source) => {
|
||||
setSelectedSource(source);
|
||||
setSelectedDate(null); // Reset selected date when source changes
|
||||
if (source !== 'shazam') {
|
||||
setSelectedChartType('top-200'); // Reset chart type for non-Shazam sources
|
||||
}
|
||||
};
|
||||
|
||||
const handleChartTypeChange = (chartType) => {
|
||||
setSelectedChartType(chartType);
|
||||
setSelectedDate(null); // Reset selected date when chart type changes
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="app">
|
||||
@ -44,11 +62,30 @@ function App() {
|
||||
|
||||
<main className="app-main">
|
||||
<div className="controls-section">
|
||||
<SourceSelector
|
||||
selectedSource={selectedSource}
|
||||
onSourceChange={handleSourceChange}
|
||||
shazamChartTypes={shazamChartTypes}
|
||||
selectedChartType={selectedChartType}
|
||||
onChartTypeChange={handleChartTypeChange}
|
||||
/>
|
||||
|
||||
<div className="year-selector-container">
|
||||
<YearSelector
|
||||
selectedYear={selectedYear}
|
||||
onYearChange={handleYearChange}
|
||||
/>
|
||||
{selectedSource !== 'shazam' && (
|
||||
<YearSelector
|
||||
selectedYear={selectedYear}
|
||||
onYearChange={handleYearChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedSource === 'shazam' && (
|
||||
<div className="shazam-info">
|
||||
<div className="info-message">
|
||||
<strong>Current Data Only</strong>
|
||||
<p>Shazam provides current trending songs. Historical data is not available.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="view-mode-selector">
|
||||
<label>View Mode:</label>
|
||||
@ -57,19 +94,19 @@ function App() {
|
||||
className={`view-mode-btn ${viewMode === 'weekly' ? 'active' : ''}`}
|
||||
onClick={() => handleViewModeChange('weekly')}
|
||||
>
|
||||
Weekly Charts
|
||||
{selectedSource === 'shazam' ? 'Current Charts' : 'Weekly Charts'}
|
||||
</button>
|
||||
<button
|
||||
className={`view-mode-btn ${viewMode === 'yearly' ? 'active' : ''}`}
|
||||
onClick={() => handleViewModeChange('yearly')}
|
||||
>
|
||||
Yearly Top Songs
|
||||
{selectedSource === 'shazam' ? 'Current Top Songs' : 'Yearly Top Songs'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{viewMode === 'weekly' && (
|
||||
{viewMode === 'weekly' && selectedSource !== 'shazam' && (
|
||||
<DateList
|
||||
dates={dates}
|
||||
selectedDate={selectedDate}
|
||||
@ -87,11 +124,11 @@ function App() {
|
||||
data={chartData}
|
||||
loading={dataLoading}
|
||||
error={dataError}
|
||||
selectedDate={selectedDate}
|
||||
selectedDate={selectedSource === 'shazam' ? 'Current' : selectedDate}
|
||||
/>
|
||||
|
||||
{chartData && chartData.length > 0 && (
|
||||
<DownloadButton data={chartData} selectedDate={selectedDate} />
|
||||
<DownloadButton data={chartData} selectedDate={selectedSource === 'shazam' ? 'Current' : selectedDate} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
@ -100,11 +137,11 @@ function App() {
|
||||
data={yearlySongs}
|
||||
loading={yearlyLoading}
|
||||
error={yearlyError}
|
||||
year={selectedYear}
|
||||
year={selectedSource === 'shazam' ? 'Current' : selectedYear}
|
||||
/>
|
||||
|
||||
{yearlySongs && yearlySongs.length > 0 && (
|
||||
<YearlyDownloadButton data={yearlySongs} year={selectedYear} />
|
||||
<YearlyDownloadButton data={yearlySongs} year={selectedSource === 'shazam' ? 'Current' : selectedYear} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
44
frontend/src/components/SourceSelector.jsx
Normal file
44
frontend/src/components/SourceSelector.jsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
const SourceSelector = ({ selectedSource, onSourceChange, shazamChartTypes, selectedChartType, onChartTypeChange }) => {
|
||||
const sources = [
|
||||
{ id: 'musicchartsarchive', name: 'Music Charts Archive', description: 'Historical Billboard charts' },
|
||||
{ id: 'kworb', name: 'Kworb', description: 'Real-time chart data' },
|
||||
{ id: 'shazam', name: 'Shazam', description: 'Current trending songs (no historical data)' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="source-selector">
|
||||
<div className="source-tabs">
|
||||
{sources.map((source) => (
|
||||
<button
|
||||
key={source.id}
|
||||
className={`source-tab ${selectedSource === source.id ? 'active' : ''}`}
|
||||
onClick={() => onSourceChange(source.id)}
|
||||
>
|
||||
<div className="source-name">{source.name}</div>
|
||||
<div className="source-description">{source.description}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedSource === 'shazam' && shazamChartTypes && (
|
||||
<div className="chart-type-selector">
|
||||
<label>Chart Type:</label>
|
||||
<select
|
||||
value={selectedChartType || 'top-200'}
|
||||
onChange={(e) => onChartTypeChange(e.target.value)}
|
||||
>
|
||||
{shazamChartTypes.map((chartType) => (
|
||||
<option key={chartType.id} value={chartType.id}>
|
||||
{chartType.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SourceSelector;
|
||||
@ -1,14 +1,14 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getChartData } from '../services/chartApi';
|
||||
|
||||
export const useChartData = (selectedDate) => {
|
||||
export const useChartData = (selectedDate, source, chartType = null) => {
|
||||
const [chartData, setChartData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchChartData = async () => {
|
||||
if (!selectedDate) {
|
||||
if (!selectedDate || !source) {
|
||||
setChartData([]);
|
||||
setError(null);
|
||||
return;
|
||||
@ -18,7 +18,7 @@ export const useChartData = (selectedDate) => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const data = await getChartData(selectedDate);
|
||||
const data = await getChartData(selectedDate, source, chartType);
|
||||
setChartData(data);
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to fetch chart data');
|
||||
@ -29,7 +29,7 @@ export const useChartData = (selectedDate) => {
|
||||
};
|
||||
|
||||
fetchChartData();
|
||||
}, [selectedDate]);
|
||||
}, [selectedDate, source, chartType]);
|
||||
|
||||
return { chartData, loading, error };
|
||||
};
|
||||
@ -1,20 +1,20 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getChartDates } from '../services/chartApi';
|
||||
|
||||
export const useChartDates = (year) => {
|
||||
export const useChartDates = (year, source) => {
|
||||
const [dates, setDates] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDates = async () => {
|
||||
if (!year) return;
|
||||
if (!year || !source) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const datesData = await getChartDates(year);
|
||||
const datesData = await getChartDates(year, source);
|
||||
setDates(datesData);
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to fetch dates');
|
||||
@ -25,7 +25,7 @@ export const useChartDates = (year) => {
|
||||
};
|
||||
|
||||
fetchDates();
|
||||
}, [year]);
|
||||
}, [year, source]);
|
||||
|
||||
return { dates, loading, error };
|
||||
};
|
||||
29
frontend/src/hooks/useShazamChartTypes.js
Normal file
29
frontend/src/hooks/useShazamChartTypes.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getShazamChartTypes } from '../services/chartApi';
|
||||
|
||||
export const useShazamChartTypes = () => {
|
||||
const [chartTypes, setChartTypes] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchChartTypes = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const data = await getShazamChartTypes();
|
||||
setChartTypes(data);
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to fetch Shazam chart types');
|
||||
setChartTypes([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchChartTypes();
|
||||
}, []);
|
||||
|
||||
return { chartTypes, loading, error };
|
||||
};
|
||||
@ -1,14 +1,14 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getYearlyTopSongs } from '../services/chartApi';
|
||||
|
||||
export const useYearlyTopSongs = (year) => {
|
||||
export const useYearlyTopSongs = (year, source, chartType = null) => {
|
||||
const [yearlySongs, setYearlySongs] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchYearlyTopSongs = async () => {
|
||||
if (!year) {
|
||||
if (!year || !source) {
|
||||
setYearlySongs([]);
|
||||
setError(null);
|
||||
return;
|
||||
@ -18,7 +18,7 @@ export const useYearlyTopSongs = (year) => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const data = await getYearlyTopSongs(year);
|
||||
const data = await getYearlyTopSongs(year, source, chartType);
|
||||
setYearlySongs(data);
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to fetch yearly top songs');
|
||||
@ -29,7 +29,7 @@ export const useYearlyTopSongs = (year) => {
|
||||
};
|
||||
|
||||
fetchYearlyTopSongs();
|
||||
}, [year]);
|
||||
}, [year, source, chartType]);
|
||||
|
||||
return { yearlySongs, loading, error };
|
||||
};
|
||||
@ -12,9 +12,9 @@ const api = axios.create({
|
||||
});
|
||||
|
||||
// Get available dates for a specific year
|
||||
export const getChartDates = async (year) => {
|
||||
export const getChartDates = async (year, source) => {
|
||||
try {
|
||||
const response = await api.get(`/chart/dates/${year}`);
|
||||
const response = await api.get(`/chart/dates/${year}?source=${source}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching chart dates:', error);
|
||||
@ -23,9 +23,13 @@ export const getChartDates = async (year) => {
|
||||
};
|
||||
|
||||
// Get chart data for a specific date
|
||||
export const getChartData = async (date) => {
|
||||
export const getChartData = async (date, source, chartType = null) => {
|
||||
try {
|
||||
const response = await api.get(`/chart/data/${date}`);
|
||||
let url = `/chart/data/${date}?source=${source}`;
|
||||
if (chartType) {
|
||||
url += `&chartType=${chartType}`;
|
||||
}
|
||||
const response = await api.get(url);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching chart data:', error);
|
||||
@ -34,9 +38,13 @@ export const getChartData = async (date) => {
|
||||
};
|
||||
|
||||
// Get yearly top songs
|
||||
export const getYearlyTopSongs = async (year) => {
|
||||
export const getYearlyTopSongs = async (year, source, chartType = null) => {
|
||||
try {
|
||||
const response = await api.get(`/chart/yearly-top/${year}`);
|
||||
let url = `/chart/yearly-top/${year}?source=${source}`;
|
||||
if (chartType) {
|
||||
url += `&chartType=${chartType}`;
|
||||
}
|
||||
const response = await api.get(url);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching yearly top songs:', error);
|
||||
@ -44,5 +52,16 @@ export const getYearlyTopSongs = async (year) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Get available Shazam chart types
|
||||
export const getShazamChartTypes = async () => {
|
||||
try {
|
||||
const response = await api.get('/chart/shazam/chart-types');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching Shazam chart types:', error);
|
||||
throw new Error(error.response?.data?.message || 'Failed to fetch Shazam chart types');
|
||||
}
|
||||
};
|
||||
|
||||
// Export the api instance for potential future use
|
||||
export default api;
|
||||
@ -54,20 +54,128 @@ body {
|
||||
|
||||
/* Controls section */
|
||||
.controls-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Year selector and view mode */
|
||||
.year-selector-container {
|
||||
/* Source selector */
|
||||
.source-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.source-tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.source-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 8px 8px 0 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.source-tab:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.source-tab.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.source-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.source-description {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart-type-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.chart-type-selector label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chart-type-selector select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.chart-type-selector select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
/* Shazam info message */
|
||||
.shazam-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #2196f3;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-message {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info-message strong {
|
||||
color: #1976d2;
|
||||
font-size: 16px;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.info-message p {
|
||||
color: #424242;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Year selector and view mode */
|
||||
.year-selector-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.year-selector {
|
||||
@ -476,8 +584,21 @@ body {
|
||||
}
|
||||
|
||||
.controls-section {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.source-tabs {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.source-tab {
|
||||
min-width: auto;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.year-selector-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.date-grid {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user